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 the fixture invocation's prepared PATH optimizer with production admission.
43    #[cfg(test)]
44    pub(crate) fn resolve_prepared(
45        profile: CanisterBuildProfile,
46    ) -> Result<Self, BuildToolchainError> {
47        let mut tools = Self::resolve(CanisterBuildProfile::Fast)?;
48        tools.profile = profile;
49        if profile == CanisterBuildProfile::Release {
50            let directories = env::var_os("PATH")
51                .map(|paths| env::split_paths(&paths).collect::<Vec<_>>())
52                .unwrap_or_default();
53            let path = crate::tool_resolution::resolve(Path::new("wasm-opt"), &directories)
54                .map_err(|source| BinaryenToolError::Io {
55                    operation: "resolve prepared fixture optimizer",
56                    path: PathBuf::from("wasm-opt"),
57                    source,
58                })?
59                .ok_or(BinaryenToolError::MissingOptimizerWithoutHome)?;
60            tools.binaryen = Some(crate::binaryen::admit_required_binaryen(&path)?);
61        }
62        Ok(tools)
63    }
64
65    /// Resolve every required tool before Cargo or artifact mutation begins.
66    pub fn resolve(profile: CanisterBuildProfile) -> Result<Self, BuildToolchainError> {
67        let ic_wasm = resolve_required_ic_wasm()?;
68        let binaryen = match profile {
69            CanisterBuildProfile::Release => Some(resolve_required_binaryen()?),
70            CanisterBuildProfile::Debug | CanisterBuildProfile::Fast => None,
71        };
72        Ok(Self {
73            profile,
74            ic_wasm,
75            binaryen,
76        })
77    }
78
79    /// Return concise exact-path diagnostics for the admitted invocation tools.
80    #[must_use]
81    pub fn diagnostic_lines(&self) -> Vec<String> {
82        let mut lines = vec![format!(
83            "ic-wasm: {} ({})",
84            self.ic_wasm.path().display(),
85            self.ic_wasm.version_identity()
86        )];
87        if let Some(binaryen) = &self.binaryen {
88            lines.push(format!(
89                "wasm-opt: {} ({})",
90                binaryen.path().display(),
91                binaryen.version_identity()
92            ));
93        }
94        lines
95    }
96
97    pub(crate) fn require_profile(
98        &self,
99        profile: CanisterBuildProfile,
100    ) -> Result<(), BuildToolchainError> {
101        if self.profile != profile {
102            return Err(BuildToolchainError::ProfileMismatch {
103                resolved: self.profile,
104                requested: profile,
105            });
106        }
107        Ok(())
108    }
109
110    pub(crate) const fn ic_wasm(&self) -> &IcWasmExecutable {
111        &self.ic_wasm
112    }
113
114    pub(crate) const fn binaryen(&self) -> Option<&BinaryenExecutable> {
115        self.binaryen.as_ref()
116    }
117}
118
119///
120/// InstalledBuildToolchain
121///
122/// Complete checksum-admitted tool set returned by the public installation command.
123///
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct InstalledBuildToolchain {
127    ic_wasm: IcWasmExecutable,
128    binaryen: BinaryenExecutable,
129    install_directory: PathBuf,
130}
131
132impl InstalledBuildToolchain {
133    #[must_use]
134    pub const fn ic_wasm(&self) -> &IcWasmExecutable {
135        &self.ic_wasm
136    }
137
138    #[must_use]
139    pub const fn binaryen(&self) -> &BinaryenExecutable {
140        &self.binaryen
141    }
142
143    #[must_use]
144    pub fn install_directory(&self) -> &Path {
145        self.install_directory.as_path()
146    }
147
148    #[must_use]
149    pub fn root_home_warning(&self) -> Option<String> {
150        let home = env::var_os("HOME");
151        root_home_warning_for(home.as_deref(), self.install_directory())
152    }
153}
154
155fn root_home_warning_for(home: Option<&OsStr>, install_directory: &Path) -> Option<String> {
156    home.filter(|home| Path::new(home) == Path::new("/"))
157        .map(|_| {
158            format!(
159                "warning: HOME resolves to `/`; tools were installed under {}. Set HOME to the intended account home and reinstall if this root-level location is accidental.",
160                install_directory.display()
161            )
162        })
163}
164
165///
166/// BuildToolchainError
167///
168/// Typed resolution, installation, or profile-binding failure for canonical build tools.
169///
170
171#[derive(Debug, ThisError)]
172pub enum BuildToolchainError {
173    #[error(transparent)]
174    Binaryen(#[from] BinaryenToolError),
175
176    #[error(transparent)]
177    IcWasm(#[from] IcWasmToolError),
178
179    #[error(
180        "build toolchain was resolved for {resolved:?}, but artifact finalization requested {requested:?}"
181    )]
182    ProfileMismatch {
183        resolved: CanisterBuildProfile,
184        requested: CanisterBuildProfile,
185    },
186
187    #[error(
188        "installed Wasm tools resolved different directories: ic-wasm at {ic_wasm}, wasm-opt at {binaryen}"
189    )]
190    SplitInstallDirectories { ic_wasm: PathBuf, binaryen: PathBuf },
191}
192
193/// Install and admit both canonical Wasm tools for the current host.
194pub fn install_required_build_toolchain() -> Result<InstalledBuildToolchain, BuildToolchainError> {
195    // Fail unsupported platform projections before downloading or publishing either tool.
196    current_binaryen_authority()?;
197    current_ic_wasm_authority()?;
198
199    let ic_wasm = install_required_ic_wasm()?;
200    let binaryen = install_required_binaryen()?;
201    let ic_wasm_directory = ic_wasm
202        .path()
203        .parent()
204        .map(Path::to_path_buf)
205        .ok_or_else(|| BuildToolchainError::SplitInstallDirectories {
206            ic_wasm: ic_wasm.path().to_path_buf(),
207            binaryen: binaryen.path().to_path_buf(),
208        })?;
209    if binaryen.path().parent() != Some(ic_wasm_directory.as_path()) {
210        return Err(BuildToolchainError::SplitInstallDirectories {
211            ic_wasm: ic_wasm.path().to_path_buf(),
212            binaryen: binaryen.path().to_path_buf(),
213        });
214    }
215    Ok(InstalledBuildToolchain {
216        ic_wasm,
217        binaryen,
218        install_directory: ic_wasm_directory,
219    })
220}