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    build_dfx_all_with_env(workspace_root, lock_relative_path, network, profile, &[]);
46}
47
48/// Build all `dfx` canisters while holding a file lock around the build and applying
49/// additional environment overrides.
50pub fn build_dfx_all_with_env(
51    workspace_root: &Path,
52    lock_relative_path: &str,
53    network: &str,
54    profile: WasmBuildProfile,
55    extra_env: &[(&str, &str)],
56) {
57    let output = run_dfx_build_with_lock(
58        workspace_root,
59        lock_relative_path,
60        network,
61        profile,
62        extra_env,
63    );
64    assert!(
65        output.status.success(),
66        "dfx build --all failed: {}",
67        String::from_utf8_lossy(&output.stderr)
68    );
69}
70
71// Walk watched files and directories and return the newest modification time.
72fn newest_watched_input_mtime(
73    workspace_root: &Path,
74    watched_relative_paths: &[&str],
75) -> io::Result<SystemTime> {
76    let mut newest = SystemTime::UNIX_EPOCH;
77
78    for relative in watched_relative_paths {
79        let path = workspace_root.join(relative);
80        newest = newest.max(newest_path_mtime(&path)?);
81    }
82
83    Ok(newest)
84}
85
86// Recursively compute the newest modification time under one watched path.
87fn newest_path_mtime(path: &Path) -> io::Result<SystemTime> {
88    let metadata = fs::metadata(path)?;
89    let mut newest = metadata.modified()?;
90
91    if metadata.is_dir() {
92        for entry in fs::read_dir(path)? {
93            let entry = entry?;
94            newest = newest.max(newest_path_mtime(&entry.path())?);
95        }
96    }
97
98    Ok(newest)
99}
100
101// Invoke `dfx canister create --all` and `dfx build --all` under one file lock when `flock`
102// is available.
103fn run_dfx_build_with_lock(
104    workspace_root: &Path,
105    lock_relative_path: &str,
106    network: &str,
107    profile: WasmBuildProfile,
108    extra_env: &[(&str, &str)],
109) -> Output {
110    let lock_file = workspace_root.join(lock_relative_path);
111    let target_dir = workspace_root.join("target/dfx-build");
112    if let Some(parent) = lock_file.parent() {
113        let _ = fs::create_dir_all(parent);
114    }
115    let _ = fs::create_dir_all(&target_dir);
116
117    let mut flock = Command::new("flock");
118    flock
119        .current_dir(workspace_root)
120        .arg(lock_file.as_os_str())
121        .arg("bash")
122        .env("DFX_NETWORK", network)
123        .env("RELEASE", profile.dfx_release_value())
124        .env("CARGO_TARGET_DIR", &target_dir)
125        .args([
126            "-lc",
127            "dfx canister create --all -qq >/dev/null 2>&1 || true\n\
128             dfx build --all",
129        ]);
130    for (key, value) in extra_env {
131        flock.env(key, value);
132    }
133
134    match flock.output() {
135        Ok(output) => output,
136        Err(err) if err.kind() == io::ErrorKind::NotFound => {
137            run_dfx_build(workspace_root, network, profile, extra_env)
138        }
139        Err(err) => panic!("failed to run `flock` for `dfx build --all`: {err}"),
140    }
141}
142
143// Invoke `dfx canister create --all` and `dfx build --all` directly when `flock` is
144// unavailable.
145fn run_dfx_build(
146    workspace_root: &Path,
147    network: &str,
148    profile: WasmBuildProfile,
149    extra_env: &[(&str, &str)],
150) -> Output {
151    let target_dir = workspace_root.join("target/dfx-build");
152    let _ = fs::create_dir_all(&target_dir);
153
154    let mut create = Command::new("dfx");
155    create
156        .current_dir(workspace_root)
157        .env("DFX_NETWORK", network)
158        .env("RELEASE", profile.dfx_release_value())
159        .env("CARGO_TARGET_DIR", &target_dir)
160        .args(["canister", "create", "--all", "-qq"]);
161    for (key, value) in extra_env {
162        create.env(key, value);
163    }
164    let _ = create.output();
165
166    let mut build = Command::new("dfx");
167    build
168        .current_dir(workspace_root)
169        .env("DFX_NETWORK", network)
170        .env("RELEASE", profile.dfx_release_value())
171        .env("CARGO_TARGET_DIR", &target_dir)
172        .args(["build", "--all"]);
173    for (key, value) in extra_env {
174        build.env(key, value);
175    }
176
177    build.output().expect("failed to run `dfx build --all`")
178}