Skip to main content

canic_host/build_toolchain/
mod.rs

1//! Module: build_toolchain
2//!
3//! Responsibility: resolve or install one exact external-tool set for a Canic build invocation.
4//! Does not own: Wasm transformation behavior, Cargo compilation, or operator shell setup.
5//! Boundary: build orchestration receives admitted absolute executables before artifact work starts.
6
7#[cfg(test)]
8mod tests;
9
10use crate::{
11    binaryen::{
12        BinaryenExecutable, BinaryenToolError, current_binaryen_authority,
13        install_required_binaryen, resolve_required_binaryen,
14    },
15    build_profile::CanisterBuildProfile,
16    ic_wasm::{
17        IcWasmExecutable, IcWasmToolError, current_ic_wasm_authority, install_required_ic_wasm,
18        resolve_required_ic_wasm,
19    },
20};
21use std::{
22    env,
23    ffi::OsStr,
24    path::{Path, PathBuf},
25};
26use thiserror::Error as ThisError;
27
28///
29/// BuildToolchain
30///
31/// Absolute admitted tools retained for every artifact transform in one build invocation.
32///
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct BuildToolchain {
36    profile: CanisterBuildProfile,
37    ic_wasm: IcWasmExecutable,
38    binaryen: Option<BinaryenExecutable>,
39}
40
41impl BuildToolchain {
42    /// Resolve every required tool before Cargo or artifact mutation begins.
43    pub fn resolve(profile: CanisterBuildProfile) -> Result<Self, BuildToolchainError> {
44        let ic_wasm = resolve_required_ic_wasm()?;
45        let binaryen = match profile {
46            CanisterBuildProfile::Release => Some(resolve_required_binaryen()?),
47            CanisterBuildProfile::Debug | CanisterBuildProfile::Fast => None,
48        };
49        Ok(Self {
50            profile,
51            ic_wasm,
52            binaryen,
53        })
54    }
55
56    /// Return concise exact-path diagnostics for the admitted invocation tools.
57    #[must_use]
58    pub fn diagnostic_lines(&self) -> Vec<String> {
59        let mut lines = vec![format!(
60            "ic-wasm: {} ({})",
61            self.ic_wasm.path().display(),
62            self.ic_wasm.version_identity()
63        )];
64        if let Some(binaryen) = &self.binaryen {
65            lines.push(format!(
66                "wasm-opt: {} ({})",
67                binaryen.path().display(),
68                binaryen.version_identity()
69            ));
70        }
71        lines
72    }
73
74    pub(crate) fn require_profile(
75        &self,
76        profile: CanisterBuildProfile,
77    ) -> Result<(), BuildToolchainError> {
78        if self.profile != profile {
79            return Err(BuildToolchainError::ProfileMismatch {
80                resolved: self.profile,
81                requested: profile,
82            });
83        }
84        Ok(())
85    }
86
87    pub(crate) const fn ic_wasm(&self) -> &IcWasmExecutable {
88        &self.ic_wasm
89    }
90
91    pub(crate) const fn binaryen(&self) -> Option<&BinaryenExecutable> {
92        self.binaryen.as_ref()
93    }
94}
95
96///
97/// InstalledBuildToolchain
98///
99/// Complete checksum-admitted tool set returned by the public installation command.
100///
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct InstalledBuildToolchain {
104    ic_wasm: IcWasmExecutable,
105    binaryen: BinaryenExecutable,
106    install_directory: PathBuf,
107}
108
109impl InstalledBuildToolchain {
110    #[must_use]
111    pub const fn ic_wasm(&self) -> &IcWasmExecutable {
112        &self.ic_wasm
113    }
114
115    #[must_use]
116    pub const fn binaryen(&self) -> &BinaryenExecutable {
117        &self.binaryen
118    }
119
120    #[must_use]
121    pub fn install_directory(&self) -> &Path {
122        self.install_directory.as_path()
123    }
124
125    #[must_use]
126    pub fn root_home_warning(&self) -> Option<String> {
127        let home = env::var_os("HOME");
128        root_home_warning_for(home.as_deref(), self.install_directory())
129    }
130}
131
132fn root_home_warning_for(home: Option<&OsStr>, install_directory: &Path) -> Option<String> {
133    home.filter(|home| Path::new(home) == Path::new("/"))
134        .map(|_| {
135            format!(
136                "warning: HOME resolves to `/`; tools were installed under {}. Set HOME to the intended account home and reinstall if this root-level location is accidental.",
137                install_directory.display()
138            )
139        })
140}
141
142///
143/// BuildToolchainError
144///
145/// Typed resolution, installation, or profile-binding failure for canonical build tools.
146///
147
148#[derive(Debug, ThisError)]
149pub enum BuildToolchainError {
150    #[error(transparent)]
151    Binaryen(#[from] BinaryenToolError),
152
153    #[error(transparent)]
154    IcWasm(#[from] IcWasmToolError),
155
156    #[error(
157        "build toolchain was resolved for {resolved:?}, but artifact finalization requested {requested:?}"
158    )]
159    ProfileMismatch {
160        resolved: CanisterBuildProfile,
161        requested: CanisterBuildProfile,
162    },
163
164    #[error(
165        "installed Wasm tools resolved different directories: ic-wasm at {ic_wasm}, wasm-opt at {binaryen}"
166    )]
167    SplitInstallDirectories { ic_wasm: PathBuf, binaryen: PathBuf },
168}
169
170/// Install and admit both canonical Wasm tools for the current host.
171pub fn install_required_build_toolchain() -> Result<InstalledBuildToolchain, BuildToolchainError> {
172    // Fail unsupported platform projections before downloading or publishing either tool.
173    current_binaryen_authority()?;
174    current_ic_wasm_authority()?;
175
176    let ic_wasm = install_required_ic_wasm()?;
177    let binaryen = install_required_binaryen()?;
178    let ic_wasm_directory = ic_wasm
179        .path()
180        .parent()
181        .map(Path::to_path_buf)
182        .ok_or_else(|| BuildToolchainError::SplitInstallDirectories {
183            ic_wasm: ic_wasm.path().to_path_buf(),
184            binaryen: binaryen.path().to_path_buf(),
185        })?;
186    if binaryen.path().parent() != Some(ic_wasm_directory.as_path()) {
187        return Err(BuildToolchainError::SplitInstallDirectories {
188            ic_wasm: ic_wasm.path().to_path_buf(),
189            binaryen: binaryen.path().to_path_buf(),
190        });
191    }
192    Ok(InstalledBuildToolchain {
193        ic_wasm,
194        binaryen,
195        install_directory: ic_wasm_directory,
196    })
197}