Skip to main content

canic_testkit/artifacts/
dfx.rs

1use super::wasm::WasmBuildProfile;
2use std::{
3    fs, io,
4    path::Path,
5    process::{Command, Output},
6    time::SystemTime,
7};
8
9/// Check whether one artifact is newer than the inputs that define it.
10pub fn artifact_is_fresh_against_inputs(
11    workspace_root: &Path,
12    artifact_path: &Path,
13    watched_relative_paths: &[&str],
14) -> io::Result<bool> {
15    let artifact_mtime = fs::metadata(artifact_path)?.modified()?;
16    let newest_input = newest_watched_input_mtime(workspace_root, watched_relative_paths)?;
17    Ok(newest_input <= artifact_mtime)
18}
19
20/// Check whether a `dfx` artifact exists and is fresh against watched inputs.
21#[must_use]
22pub fn dfx_artifact_ready(
23    workspace_root: &Path,
24    artifact_relative_path: &str,
25    watched_relative_paths: &[&str],
26) -> bool {
27    let artifact_path = workspace_root.join(artifact_relative_path);
28
29    match fs::metadata(&artifact_path) {
30        Ok(meta) if meta.is_file() && meta.len() > 0 => {
31            artifact_is_fresh_against_inputs(workspace_root, &artifact_path, watched_relative_paths)
32                .unwrap_or(false)
33        }
34        _ => false,
35    }
36}
37
38/// Build all `dfx` canisters while holding a file lock around the build.
39pub fn build_dfx_all(
40    workspace_root: &Path,
41    lock_relative_path: &str,
42    network: &str,
43    profile: WasmBuildProfile,
44) {
45    let output = run_dfx_build_with_lock(workspace_root, lock_relative_path, network, profile);
46    assert!(
47        output.status.success(),
48        "dfx build --all failed: {}",
49        String::from_utf8_lossy(&output.stderr)
50    );
51}
52
53// Walk watched files and directories and return the newest modification time.
54fn newest_watched_input_mtime(
55    workspace_root: &Path,
56    watched_relative_paths: &[&str],
57) -> io::Result<SystemTime> {
58    let mut newest = SystemTime::UNIX_EPOCH;
59
60    for relative in watched_relative_paths {
61        let path = workspace_root.join(relative);
62        newest = newest.max(newest_path_mtime(&path)?);
63    }
64
65    Ok(newest)
66}
67
68// Recursively compute the newest modification time under one watched path.
69fn newest_path_mtime(path: &Path) -> io::Result<SystemTime> {
70    let metadata = fs::metadata(path)?;
71    let mut newest = metadata.modified()?;
72
73    if metadata.is_dir() {
74        for entry in fs::read_dir(path)? {
75            let entry = entry?;
76            newest = newest.max(newest_path_mtime(&entry.path())?);
77        }
78    }
79
80    Ok(newest)
81}
82
83// Invoke `dfx canister create --all` and `dfx build --all` under one file lock when `flock`
84// is available.
85fn run_dfx_build_with_lock(
86    workspace_root: &Path,
87    lock_relative_path: &str,
88    network: &str,
89    profile: WasmBuildProfile,
90) -> Output {
91    let lock_file = workspace_root.join(lock_relative_path);
92    let target_dir = workspace_root.join("target/dfx-build");
93    if let Some(parent) = lock_file.parent() {
94        let _ = fs::create_dir_all(parent);
95    }
96    let _ = fs::create_dir_all(&target_dir);
97
98    match Command::new("flock")
99        .current_dir(workspace_root)
100        .arg(lock_file.as_os_str())
101        .arg("bash")
102        .env("DFX_NETWORK", network)
103        .env("RELEASE", profile.dfx_release_value())
104        .env("CARGO_TARGET_DIR", &target_dir)
105        .args([
106            "-lc",
107            "dfx canister create --all -qq >/dev/null 2>&1 || true\n\
108             dfx build --all",
109        ])
110        .output()
111    {
112        Ok(output) => output,
113        Err(err) if err.kind() == io::ErrorKind::NotFound => {
114            run_dfx_build(workspace_root, network, profile)
115        }
116        Err(err) => panic!("failed to run `flock` for `dfx build --all`: {err}"),
117    }
118}
119
120// Invoke `dfx canister create --all` and `dfx build --all` directly when `flock` is
121// unavailable.
122fn run_dfx_build(workspace_root: &Path, network: &str, profile: WasmBuildProfile) -> Output {
123    let target_dir = workspace_root.join("target/dfx-build");
124    let _ = fs::create_dir_all(&target_dir);
125
126    let _ = Command::new("dfx")
127        .current_dir(workspace_root)
128        .env("DFX_NETWORK", network)
129        .env("RELEASE", profile.dfx_release_value())
130        .env("CARGO_TARGET_DIR", &target_dir)
131        .args(["canister", "create", "--all", "-qq"])
132        .output();
133
134    Command::new("dfx")
135        .current_dir(workspace_root)
136        .env("DFX_NETWORK", network)
137        .env("RELEASE", profile.dfx_release_value())
138        .env("CARGO_TARGET_DIR", &target_dir)
139        .args(["build", "--all"])
140        .output()
141        .expect("failed to run `dfx build --all`")
142}