anodizer_core/
arch_path_guard.rs1use std::collections::HashSet;
23use std::path::{Path, PathBuf};
24
25#[derive(Debug, Default)]
28pub struct ArchPathGuard {
29 seen: HashSet<PathBuf>,
30}
31
32impl ArchPathGuard {
33 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn check(
47 &mut self,
48 path: &Path,
49 stage: &str,
50 artifact: &str,
51 name_template: &str,
52 rendered: &str,
53 crate_name: &str,
54 ) -> anyhow::Result<()> {
55 if self.seen.insert(path.to_path_buf()) {
56 return Ok(());
57 }
58 anyhow::bail!(
59 "{stage}: name template '{name_template}' rendered the same {artifact} \
60 '{rendered}' more than once for crate '{crate_name}', so one build target \
61 would silently overwrite another. Add '{{{{ .Arch }}}}' to the `name` \
62 (e.g. \"{{{{ .ProjectName }}}}_{{{{ .Arch }}}}\") — or, when the collision \
63 is between same-arch micro-architecture variants, '{{{{ .Amd64 }}}}' \
64 (e.g. \"{{{{ .ProjectName }}}}_{{{{ .Arch }}}}{{{{ .Amd64 }}}}\") — so each \
65 build target's {artifact} gets a distinct path."
66 );
67 }
68
69 pub fn check_conventional(
80 &mut self,
81 path: &Path,
82 stage: &str,
83 artifact: &str,
84 rendered: &str,
85 crate_name: &str,
86 ) -> anyhow::Result<()> {
87 if self.seen.insert(path.to_path_buf()) {
88 return Ok(());
89 }
90 anyhow::bail!(
91 "{stage}: the conventional default filename rendered the same {artifact} \
92 '{rendered}' more than once for crate '{crate_name}', so one build target \
93 would silently overwrite another. This happens when two same-arch \
94 micro-architecture variants (e.g. a baseline amd64 build and one tuned \
95 with -Ctarget-cpu=x86-64-v3) share the distro-conventional arch field. \
96 Set a `file_name_template` carrying '{{{{ .Amd64 }}}}' \
97 (e.g. \"{{{{ .PackageName }}}}_{{{{ .Version }}}}_{{{{ .Arch }}}}{{{{ .Amd64 }}}}\") \
98 so each variant's {artifact} gets a distinct path."
99 );
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn distinct_paths_pass() {
109 let mut guard = ArchPathGuard::new();
110 guard
111 .check(
112 Path::new("dist/macos/app_amd64.app"),
113 "app_bundles",
114 "bundle",
115 "{{ .ProjectName }}_{{ .Arch }}",
116 "app_amd64.app",
117 "app",
118 )
119 .expect("first path must pass");
120 guard
121 .check(
122 Path::new("dist/macos/app_arm64.app"),
123 "app_bundles",
124 "bundle",
125 "{{ .ProjectName }}_{{ .Arch }}",
126 "app_arm64.app",
127 "app",
128 )
129 .expect("distinct second path must pass");
130 }
131
132 #[test]
133 fn duplicate_path_bails_with_actionable_message() {
134 let mut guard = ArchPathGuard::new();
135 guard
136 .check(
137 Path::new("dist/macos/app.app"),
138 "app_bundles",
139 "bundle",
140 "{{ .ProjectName }}",
141 "app.app",
142 "app",
143 )
144 .expect("first path must pass");
145
146 let err = guard
147 .check(
148 Path::new("dist/macos/app.app"),
149 "app_bundles",
150 "bundle",
151 "{{ .ProjectName }}",
152 "app.app",
153 "app",
154 )
155 .unwrap_err()
156 .to_string();
157
158 assert!(err.contains("app_bundles:"), "{err}");
159 assert!(err.contains("crate 'app'"), "{err}");
160 assert!(err.contains("{{ .Arch }}"), "{err}");
161 assert!(err.contains("bundle gets a distinct path"), "{err}");
162 }
163
164 #[test]
165 fn conventional_default_collision_bails_without_fake_template() {
166 let mut guard = ArchPathGuard::new();
170 guard
171 .check_conventional(
172 Path::new("dist/linux/myapp_1.0.0_amd64.deb"),
173 "nfpms",
174 "package",
175 "myapp_1.0.0_amd64.deb",
176 "myapp",
177 )
178 .expect("first path must pass");
179
180 let err = guard
181 .check_conventional(
182 Path::new("dist/linux/myapp_1.0.0_amd64.deb"),
183 "nfpms",
184 "package",
185 "myapp_1.0.0_amd64.deb",
186 "myapp",
187 )
188 .unwrap_err()
189 .to_string();
190
191 assert!(err.contains("nfpms:"), "{err}");
192 assert!(err.contains("crate 'myapp'"), "{err}");
193 assert!(err.contains("conventional default filename"), "{err}");
194 assert!(err.contains("{{ .Amd64 }}"), "{err}");
195 assert!(
196 !err.contains("name template '"),
197 "must not echo a fake template: {err}"
198 );
199 assert!(err.contains("package gets a distinct path"), "{err}");
200 }
201
202 #[test]
203 fn separate_scopes_do_not_share_state() {
204 let path = Path::new("dist/macos/app.app");
208 let mut first = ArchPathGuard::new();
209 first
210 .check(path, "dmgs", "image", "{{ .ProjectName }}", "app.dmg", "a")
211 .expect("scope one first write");
212 let mut second = ArchPathGuard::new();
213 second
214 .check(path, "dmgs", "image", "{{ .ProjectName }}", "app.dmg", "b")
215 .expect("scope two first write must pass");
216 }
217}