1
1
//! Tidy check to ensure paths mentioned in triagebot.toml exist in the project.
2
2
3
- use std:: path:: Path ;
3
+ use std:: collections:: HashMap ;
4
+ use std:: path:: { Path , PathBuf } ;
4
5
6
+ use build_helper:: ci:: CiEnv ;
5
7
use toml:: Value ;
6
8
7
- pub fn check ( path : & Path , bad : & mut bool ) {
8
- let triagebot_path = path . join ( "triagebot.toml" ) ;
9
+ pub fn check ( checkout_root : & Path , bad : & mut bool ) {
10
+ let triagebot_path = checkout_root . join ( "triagebot.toml" ) ;
9
11
if !triagebot_path. exists ( ) {
10
12
tidy_error ! ( bad, "triagebot.toml file not found" ) ;
11
13
return ;
@@ -14,73 +16,91 @@ pub fn check(path: &Path, bad: &mut bool) {
14
16
let contents = std:: fs:: read_to_string ( & triagebot_path) . unwrap ( ) ;
15
17
let config: Value = toml:: from_str ( & contents) . unwrap ( ) ;
16
18
17
- // Check [mentions."*"] sections, i.e. [mentions."compiler/rustc_const_eval/src/"]
19
+ // Cache mapping between submodule path <-> whether submodule is checked out. This cache is to
20
+ // avoid excessive filesystem accesses.
21
+ let submodule_checked_out_status = cache_submodule_checkout_status ( checkout_root) ;
22
+
23
+ // Check `[mentions."*"]` sections, i.e.
24
+ //
25
+ // ```
26
+ // [mentions."compiler/rustc_const_eval/src/"]
27
+ // ```
18
28
if let Some ( Value :: Table ( mentions) ) = config. get ( "mentions" ) {
19
29
for path_str in mentions. keys ( ) {
20
30
// Remove quotes from the path
21
31
let clean_path = path_str. trim_matches ( '"' ) ;
22
- let full_path = path . join ( clean_path) ;
32
+ let full_path = checkout_root . join ( clean_path) ;
23
33
24
- if !full_path . exists ( ) {
34
+ if !check_path_exists_if_required ( & submodule_checked_out_status , & full_path ) {
25
35
tidy_error ! (
26
36
bad,
27
- "triagebot.toml [mentions.*] contains path '{}' which doesn't exist" ,
37
+ "triagebot.toml ` [mentions.*]` contains path `{}` which doesn't exist" ,
28
38
clean_path
29
39
) ;
30
40
}
31
41
}
32
42
} else {
33
43
tidy_error ! (
34
44
bad,
35
- "triagebot.toml missing [mentions.*] section, this wrong for rust-lang/rust repo."
45
+ "`triagebot.toml` is missing the `[mentions.*]` section; this is wrong for the \
46
+ `rust-lang/rust` repo."
36
47
) ;
37
48
}
38
49
39
- // Check [assign.owners] sections, i.e.
50
+ // Check `[assign.owners]` sections, i.e.
51
+ //
52
+ // ```
40
53
// [assign.owners]
41
54
// "/.github/workflows" = ["infra-ci"]
55
+ // ```
42
56
if let Some ( Value :: Table ( assign) ) = config. get ( "assign" ) {
43
57
if let Some ( Value :: Table ( owners) ) = assign. get ( "owners" ) {
44
58
for path_str in owners. keys ( ) {
45
59
// Remove quotes and leading slash from the path
46
60
let clean_path = path_str. trim_matches ( '"' ) . trim_start_matches ( '/' ) ;
47
- let full_path = path . join ( clean_path) ;
61
+ let full_path = checkout_root . join ( clean_path) ;
48
62
49
- if !full_path . exists ( ) {
63
+ if !check_path_exists_if_required ( & submodule_checked_out_status , & full_path ) {
50
64
tidy_error ! (
51
65
bad,
52
- "triagebot.toml [assign.owners] contains path '{}' which doesn't exist" ,
66
+ "` triagebot.toml` ` [assign.owners]` contains path `{}` which doesn't exist" ,
53
67
clean_path
54
68
) ;
55
69
}
56
70
}
57
71
} else {
58
72
tidy_error ! (
59
73
bad,
60
- "triagebot.toml missing [assign.owners] section, this wrong for rust-lang/rust repo."
74
+ "`triagebot.toml` is missing the `[assign.owners]` section; this is wrong for the \
75
+ `rust-lang/rust` repo."
61
76
) ;
62
77
}
63
78
}
64
79
65
- // Verify that trigger_files in [autolabel."*"] exist in the project, i.e.
80
+ // Verify that `trigger_files` paths in `[autolabel."*"]` exists, i.e.
81
+ //
82
+ // ```
66
83
// [autolabel."A-rustdoc-search"]
67
84
// trigger_files = [
68
85
// "src/librustdoc/html/static/js/search.js",
69
86
// "tests/rustdoc-js",
70
87
// "tests/rustdoc-js-std",
71
88
// ]
89
+ // ```
72
90
if let Some ( Value :: Table ( autolabels) ) = config. get ( "autolabel" ) {
73
91
for ( label, content) in autolabels {
74
92
if let Some ( trigger_files) = content. get ( "trigger_files" ) . and_then ( |v| v. as_array ( ) ) {
75
93
for file in trigger_files {
76
94
if let Some ( file_str) = file. as_str ( ) {
77
- let full_path = path . join ( file_str) ;
95
+ let full_path = checkout_root . join ( file_str) ;
78
96
79
97
// Handle both file and directory paths
80
- if !full_path. exists ( ) {
98
+ if !check_path_exists_if_required ( & submodule_checked_out_status, & full_path)
99
+ {
81
100
tidy_error ! (
82
101
bad,
83
- "triagebot.toml [autolabel.{}] contains trigger_files path '{}' which doesn't exist" ,
102
+ "`triagebot.toml` `[autolabel.{}]` contains `trigger_files` path \
103
+ `{}` which doesn't exist",
84
104
label,
85
105
file_str
86
106
) ;
@@ -91,3 +111,61 @@ pub fn check(path: &Path, bad: &mut bool) {
91
111
}
92
112
}
93
113
}
114
+
115
+ /// Very naive heuristics for whether a submodule is checked out.
116
+ fn cache_submodule_checkout_status ( checkout_root : & Path ) -> HashMap < PathBuf , bool > {
117
+ let mut cache = HashMap :: default ( ) ;
118
+
119
+ // NOTE: can't assume `git` exists.
120
+ let submodule_paths = build_helper:: util:: parse_gitmodules ( & checkout_root) ;
121
+
122
+ for submodule in submodule_paths {
123
+ let full_submodule_path = checkout_root. join ( submodule) ;
124
+
125
+ let is_checked_out = if CiEnv :: is_ci ( ) {
126
+ // In CI, require all submodules to be checked out and thus don't skip checking any
127
+ // paths.
128
+ true
129
+ } else {
130
+ // NOTE: for our purposes, just skip checking paths to and under a submodule if we can't
131
+ // read its dir locally (even if this can miss broken paths).
132
+ std:: fs:: read_dir ( & full_submodule_path) . is_ok_and ( |entry| {
133
+ // NOTE: de-initializing a submodule can leave an empty folder behind
134
+ entry. count ( ) > 0
135
+ } )
136
+ } ;
137
+
138
+ if let Some ( _) = cache. insert ( full_submodule_path. clone ( ) , is_checked_out) {
139
+ panic ! (
140
+ "unexpected duplicate submodule paths in `deps::WORKSPACES`: {} already in \
141
+ submodule checkout cache",
142
+ full_submodule_path. display( )
143
+ ) ;
144
+ }
145
+ }
146
+
147
+ cache
148
+ }
149
+
150
+ /// Check that a path exists. This is:
151
+ ///
152
+ /// - Unconditionally checked under CI environment.
153
+ /// - Only checked under local environment if submodule is checked out (if candidate path points
154
+ /// under or to a submodule).
155
+ fn check_path_exists_if_required (
156
+ submodule_checkout_status : & HashMap < PathBuf , bool > ,
157
+ candidate : & Path ,
158
+ ) -> bool {
159
+ for ( submodule_path, is_checked_out) in submodule_checkout_status {
160
+ if candidate. starts_with ( submodule_path) {
161
+ if * is_checked_out {
162
+ return candidate. exists ( ) ;
163
+ } else {
164
+ // Not actually checked, but just skipped.
165
+ return true ;
166
+ }
167
+ }
168
+ }
169
+
170
+ candidate. exists ( )
171
+ }
0 commit comments