1use super::wasm::WasmBuildProfile;
2use std::{
3 fs, io,
4 path::Path,
5 process::{Command, Output},
6 time::SystemTime,
7};
8
9const DFX_BUILD_ENV_STAMP_RELATIVE: &str = ".dfx/canic-build-env.stamp";
10
11#[derive(Clone, Copy, Debug)]
16pub struct WatchedInputSnapshot {
17 newest_input_mtime: SystemTime,
18}
19
20impl WatchedInputSnapshot {
21 pub fn capture(workspace_root: &Path, watched_relative_paths: &[&str]) -> io::Result<Self> {
23 Ok(Self {
24 newest_input_mtime: newest_watched_input_mtime(workspace_root, watched_relative_paths)?,
25 })
26 }
27
28 pub fn artifact_is_fresh(self, artifact_path: &Path) -> io::Result<bool> {
30 let artifact_mtime = fs::metadata(artifact_path)?.modified()?;
31 Ok(self.newest_input_mtime <= artifact_mtime)
32 }
33}
34
35#[must_use]
37pub fn dfx_artifact_ready_for_build(
38 workspace_root: &Path,
39 artifact_relative_path: &str,
40 watched_relative_paths: &[&str],
41 network: &str,
42 profile: WasmBuildProfile,
43 extra_env: &[(&str, &str)],
44) -> bool {
45 let Ok(watched_inputs) = WatchedInputSnapshot::capture(workspace_root, watched_relative_paths)
46 else {
47 return false;
48 };
49
50 dfx_artifact_ready_with_snapshot(
51 workspace_root,
52 artifact_relative_path,
53 watched_inputs,
54 network,
55 profile,
56 extra_env,
57 )
58}
59
60#[must_use]
62pub fn dfx_artifact_ready_with_snapshot(
63 workspace_root: &Path,
64 artifact_relative_path: &str,
65 watched_inputs: WatchedInputSnapshot,
66 network: &str,
67 profile: WasmBuildProfile,
68 extra_env: &[(&str, &str)],
69) -> bool {
70 let artifact_path = workspace_root.join(artifact_relative_path);
71
72 match fs::metadata(&artifact_path) {
73 Ok(meta) if meta.is_file() && meta.len() > 0 => {
74 watched_inputs
75 .artifact_is_fresh(&artifact_path)
76 .unwrap_or(false)
77 && build_stamp_matches(workspace_root, network, profile, extra_env)
78 }
79 _ => false,
80 }
81}
82
83pub fn build_dfx_all_with_env(
86 workspace_root: &Path,
87 lock_relative_path: &str,
88 network: &str,
89 profile: WasmBuildProfile,
90 extra_env: &[(&str, &str)],
91) {
92 let output = run_local_artifact_build_with_lock(
93 workspace_root,
94 lock_relative_path,
95 network,
96 profile,
97 extra_env,
98 );
99 assert!(
100 output.status.success(),
101 "local artifact build failed: {}",
102 String::from_utf8_lossy(&output.stderr)
103 );
104 write_build_stamp(workspace_root, network, profile, extra_env)
105 .expect("write local artifact build env stamp");
106}
107
108fn newest_watched_input_mtime(
110 workspace_root: &Path,
111 watched_relative_paths: &[&str],
112) -> io::Result<SystemTime> {
113 let mut newest = SystemTime::UNIX_EPOCH;
114
115 for relative in watched_relative_paths {
116 let path = workspace_root.join(relative);
117 newest = newest.max(newest_path_mtime(&path)?);
118 }
119
120 Ok(newest)
121}
122
123fn newest_path_mtime(path: &Path) -> io::Result<SystemTime> {
125 let metadata = fs::metadata(path)?;
126 let mut newest = metadata.modified()?;
127
128 if metadata.is_dir() {
129 for entry in fs::read_dir(path)? {
130 let entry = entry?;
131 newest = newest.max(newest_path_mtime(&entry.path())?);
132 }
133 }
134
135 Ok(newest)
136}
137
138fn build_stamp_matches(
139 workspace_root: &Path,
140 network: &str,
141 profile: WasmBuildProfile,
142 extra_env: &[(&str, &str)],
143) -> bool {
144 fs::read_to_string(workspace_root.join(DFX_BUILD_ENV_STAMP_RELATIVE))
145 .map(|current| current == build_stamp_contents(network, profile, extra_env))
146 .unwrap_or(false)
147}
148
149fn write_build_stamp(
150 workspace_root: &Path,
151 network: &str,
152 profile: WasmBuildProfile,
153 extra_env: &[(&str, &str)],
154) -> io::Result<()> {
155 let stamp_path = workspace_root.join(DFX_BUILD_ENV_STAMP_RELATIVE);
156 if let Some(parent) = stamp_path.parent() {
157 fs::create_dir_all(parent)?;
158 }
159 fs::write(
160 stamp_path,
161 build_stamp_contents(network, profile, extra_env),
162 )
163}
164
165fn build_stamp_contents(
166 network: &str,
167 profile: WasmBuildProfile,
168 extra_env: &[(&str, &str)],
169) -> String {
170 let mut lines = vec![
171 format!("DFX_NETWORK={network}"),
172 format!("CANIC_WASM_PROFILE={}", profile.canic_wasm_profile_value()),
173 ];
174
175 let mut extra = extra_env.to_vec();
176 extra.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
177 lines.extend(
178 extra
179 .into_iter()
180 .map(|(key, value)| format!("{key}={value}")),
181 );
182 lines.push(String::new());
183 lines.join("\n")
184}
185
186fn run_local_artifact_build_with_lock(
188 workspace_root: &Path,
189 lock_relative_path: &str,
190 network: &str,
191 profile: WasmBuildProfile,
192 extra_env: &[(&str, &str)],
193) -> Output {
194 let lock_file = workspace_root.join(lock_relative_path);
195 let target_dir = workspace_root.join("target/dfx-build");
196 if let Some(parent) = lock_file.parent() {
197 let _ = fs::create_dir_all(parent);
198 }
199 let _ = fs::create_dir_all(&target_dir);
200
201 let mut flock = Command::new("flock");
202 flock
203 .current_dir(workspace_root)
204 .arg(lock_file.as_os_str())
205 .arg("bash")
206 .env("DFX_NETWORK", network)
207 .env("CANIC_WASM_PROFILE", profile.canic_wasm_profile_value())
208 .env("CARGO_TARGET_DIR", &target_dir)
209 .arg("scripts/ci/build-ci-wasm-artifacts.sh");
210 for (key, value) in extra_env {
211 flock.env(key, value);
212 }
213
214 match flock.output() {
215 Ok(output) => output,
216 Err(err) if err.kind() == io::ErrorKind::NotFound => {
217 run_local_artifact_build(workspace_root, network, profile, extra_env)
218 }
219 Err(err) => panic!("failed to run `flock` for local artifact build: {err}"),
220 }
221}
222
223fn run_local_artifact_build(
225 workspace_root: &Path,
226 network: &str,
227 profile: WasmBuildProfile,
228 extra_env: &[(&str, &str)],
229) -> Output {
230 let target_dir = workspace_root.join("target/dfx-build");
231 let _ = fs::create_dir_all(&target_dir);
232
233 let mut build = Command::new("bash");
234 build
235 .current_dir(workspace_root)
236 .env("DFX_NETWORK", network)
237 .env("CANIC_WASM_PROFILE", profile.canic_wasm_profile_value())
238 .env("CARGO_TARGET_DIR", &target_dir)
239 .arg("scripts/ci/build-ci-wasm-artifacts.sh");
240 for (key, value) in extra_env {
241 build.env(key, value);
242 }
243
244 build
245 .output()
246 .expect("failed to run local artifact build helper")
247}
248
249#[cfg(test)]
250mod tests {
251 use super::{build_stamp_contents, dfx_artifact_ready_for_build};
252 use crate::artifacts::WasmBuildProfile;
253 use std::{
254 fs,
255 path::PathBuf,
256 thread::sleep,
257 time::Duration,
258 time::{SystemTime, UNIX_EPOCH},
259 };
260
261 fn temp_workspace() -> PathBuf {
262 let unique = SystemTime::now()
263 .duration_since(UNIX_EPOCH)
264 .expect("system time before epoch")
265 .as_nanos();
266 let path = std::env::temp_dir().join(format!("canic-dfx-artifact-test-{unique}"));
267 fs::create_dir_all(path.join(".dfx/local/canisters/root")).expect("create temp workspace");
268 path
269 }
270
271 #[test]
272 fn dfx_artifact_ready_requires_matching_build_env_stamp() {
273 let workspace_root = temp_workspace();
274 let artifact_relative_path = ".dfx/local/canisters/root/root.wasm.gz";
275 let artifact_path = workspace_root.join(artifact_relative_path);
276 fs::write(workspace_root.join("Cargo.toml"), "workspace").expect("write watched input");
277 sleep(Duration::from_millis(20));
278 fs::write(&artifact_path, b"wasm").expect("write artifact");
279 fs::write(
280 workspace_root.join(".dfx/canic-build-env.stamp"),
281 build_stamp_contents("local", WasmBuildProfile::Debug, &[]),
282 )
283 .expect("write build stamp");
284
285 assert!(dfx_artifact_ready_for_build(
286 &workspace_root,
287 artifact_relative_path,
288 &["Cargo.toml"],
289 "local",
290 WasmBuildProfile::Debug,
291 &[],
292 ));
293 assert!(!dfx_artifact_ready_for_build(
294 &workspace_root,
295 artifact_relative_path,
296 &["Cargo.toml"],
297 "local",
298 WasmBuildProfile::Debug,
299 &[("RUSTFLAGS", "--cfg canic_test_small_wasm_store")],
300 ));
301
302 let _ = fs::remove_dir_all(workspace_root);
303 }
304}