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 .is_ok_and(|current| current == build_stamp_contents(network, profile, extra_env))
146}
147
148fn write_build_stamp(
149 workspace_root: &Path,
150 network: &str,
151 profile: WasmBuildProfile,
152 extra_env: &[(&str, &str)],
153) -> io::Result<()> {
154 let stamp_path = workspace_root.join(DFX_BUILD_ENV_STAMP_RELATIVE);
155 if let Some(parent) = stamp_path.parent() {
156 fs::create_dir_all(parent)?;
157 }
158 fs::write(
159 stamp_path,
160 build_stamp_contents(network, profile, extra_env),
161 )
162}
163
164fn build_stamp_contents(
165 network: &str,
166 profile: WasmBuildProfile,
167 extra_env: &[(&str, &str)],
168) -> String {
169 let mut lines = vec![
170 format!("DFX_NETWORK={network}"),
171 format!("CANIC_WASM_PROFILE={}", profile.canic_wasm_profile_value()),
172 ];
173
174 let mut extra = extra_env.to_vec();
175 extra.sort_unstable_by_key(|(left, _)| *left);
176 lines.extend(
177 extra
178 .into_iter()
179 .map(|(key, value)| format!("{key}={value}")),
180 );
181 lines.push(String::new());
182 lines.join("\n")
183}
184
185fn run_local_artifact_build_with_lock(
187 workspace_root: &Path,
188 lock_relative_path: &str,
189 network: &str,
190 profile: WasmBuildProfile,
191 extra_env: &[(&str, &str)],
192) -> Output {
193 let lock_file = workspace_root.join(lock_relative_path);
194 let target_dir = workspace_root.join("target/dfx-build");
195 if let Some(parent) = lock_file.parent() {
196 let _ = fs::create_dir_all(parent);
197 }
198 let _ = fs::create_dir_all(&target_dir);
199
200 let mut flock = Command::new("flock");
201 flock
202 .current_dir(workspace_root)
203 .arg(lock_file.as_os_str())
204 .arg("bash")
205 .env("DFX_NETWORK", network)
206 .env("CANIC_WASM_PROFILE", profile.canic_wasm_profile_value())
207 .env("CARGO_TARGET_DIR", &target_dir)
208 .arg("scripts/ci/build-ci-wasm-artifacts.sh");
209 for (key, value) in extra_env {
210 flock.env(key, value);
211 }
212
213 match flock.output() {
214 Ok(output) => output,
215 Err(err) if err.kind() == io::ErrorKind::NotFound => {
216 run_local_artifact_build(workspace_root, network, profile, extra_env)
217 }
218 Err(err) => panic!("failed to run `flock` for local artifact build: {err}"),
219 }
220}
221
222fn run_local_artifact_build(
224 workspace_root: &Path,
225 network: &str,
226 profile: WasmBuildProfile,
227 extra_env: &[(&str, &str)],
228) -> Output {
229 let target_dir = workspace_root.join("target/dfx-build");
230 let _ = fs::create_dir_all(&target_dir);
231
232 let mut build = Command::new("bash");
233 build
234 .current_dir(workspace_root)
235 .env("DFX_NETWORK", network)
236 .env("CANIC_WASM_PROFILE", profile.canic_wasm_profile_value())
237 .env("CARGO_TARGET_DIR", &target_dir)
238 .arg("scripts/ci/build-ci-wasm-artifacts.sh");
239 for (key, value) in extra_env {
240 build.env(key, value);
241 }
242
243 build
244 .output()
245 .expect("failed to run local artifact build helper")
246}
247
248#[cfg(test)]
249mod tests {
250 use super::{build_stamp_contents, dfx_artifact_ready_for_build};
251 use crate::artifacts::WasmBuildProfile;
252 use std::{
253 fs,
254 path::PathBuf,
255 thread::sleep,
256 time::Duration,
257 time::{SystemTime, UNIX_EPOCH},
258 };
259
260 fn temp_workspace() -> PathBuf {
261 let unique = SystemTime::now()
262 .duration_since(UNIX_EPOCH)
263 .expect("system time before epoch")
264 .as_nanos();
265 let path = std::env::temp_dir().join(format!("canic-dfx-artifact-test-{unique}"));
266 fs::create_dir_all(path.join(".dfx/local/canisters/root")).expect("create temp workspace");
267 path
268 }
269
270 #[test]
271 fn dfx_artifact_ready_requires_matching_build_env_stamp() {
272 let workspace_root = temp_workspace();
273 let artifact_relative_path = ".dfx/local/canisters/root/root.wasm.gz";
274 let artifact_path = workspace_root.join(artifact_relative_path);
275 fs::write(workspace_root.join("Cargo.toml"), "workspace").expect("write watched input");
276 sleep(Duration::from_millis(20));
277 fs::write(&artifact_path, b"wasm").expect("write artifact");
278 fs::write(
279 workspace_root.join(".dfx/canic-build-env.stamp"),
280 build_stamp_contents("local", WasmBuildProfile::Debug, &[]),
281 )
282 .expect("write build stamp");
283
284 assert!(dfx_artifact_ready_for_build(
285 &workspace_root,
286 artifact_relative_path,
287 &["Cargo.toml"],
288 "local",
289 WasmBuildProfile::Debug,
290 &[],
291 ));
292 assert!(!dfx_artifact_ready_for_build(
293 &workspace_root,
294 artifact_relative_path,
295 &["Cargo.toml"],
296 "local",
297 WasmBuildProfile::Debug,
298 &[("RUSTFLAGS", "--cfg canic_test_small_wasm_store")],
299 ));
300
301 let _ = fs::remove_dir_all(workspace_root);
302 }
303}