Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions anneal/v2/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

use clap::Parser as _;

#[allow(dead_code)]
mod setup;
#[allow(dead_code)]
mod util;

Expand Down Expand Up @@ -60,10 +62,11 @@ fn setup_installation_dir(args: SetupArgs) -> std::path::PathBuf {
None => exocrate::Source::Remote(REMOTE),
};

CONFIG
let (installation_dir, _) = CONFIG
.resolve_installation_dir_or_install(location, source)
// FIXME: Implement unified error reporting (e.g., via `anyhow`).
.expect("failed to resolve-or-install dependencies")
.expect("failed to resolve-or-install dependencies");
installation_dir
}

fn setup(args: SetupArgs) {
Expand Down
154 changes: 154 additions & 0 deletions anneal/v2/src/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2026 The Fuchsia Authors
//
// Licensed under the 2-Clause BSD License <LICENSE-BSD or
// https://opensource.org/license/bsd-2-clause>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

use anyhow::Context as _;

pub struct SetupArgs {
pub local_archive: Option<std::path::PathBuf>,
}

exocrate::config! {
pub const CONFIG: Config = Config {
rel_dir_path: [".anneal", "toolchain"],
versioned_files: &["../Cargo.toml", "../Cargo.lock"],
};
}

exocrate::parse_remote_archive! {
pub const REMOTE: RemoteArchive = "Cargo.toml" [
(linux, x86_64),
(macos, x86_64),
(linux, aarch64),
(macos, aarch64),
];
}

pub enum Tool {
Cargo,
Charon,
Rustc,
}

impl Tool {
pub fn name(&self) -> &'static str {
match self {
Self::Cargo => "cargo",
Self::Charon => "charon",
Self::Rustc => "rustc",
}
}

pub fn path(&self, toolchain: &Toolchain) -> std::path::PathBuf {
match self {
Self::Cargo | Self::Rustc => toolchain.rust_bin().join(self.name()),
Self::Charon => toolchain.aeneas_bin_dir().join(self.name()),
}
}
}

const AENEAS_DIR: &str = "aeneas";
const RUST_SYSROOT: &str = "rust";
const AENEAS_BIN_DIR: &str = "bin";
const RUST_BIN_DIR: &str = "bin";
const RUST_LIB_DIR: &str = "lib";

pub struct Toolchain {
root: std::path::PathBuf,
}

impl Toolchain {
pub fn resolve() -> anyhow::Result<Self> {
let location = resolve_location();
let root = CONFIG
.resolve_installation_dir(location)
.context("Toolchain not installed. Please run 'cargo anneal setup' first.")?;
Ok(Self { root })
}

#[cfg(all(test, feature = "exocrate_tests"))]
pub fn root(&self) -> &std::path::Path {
&self.root
}

pub fn aeneas_bin_dir(&self) -> std::path::PathBuf {
self.root.join(AENEAS_DIR).join(AENEAS_BIN_DIR)
}

pub fn rust_sysroot(&self) -> std::path::PathBuf {
self.root.join(RUST_SYSROOT)
}

pub fn rust_bin(&self) -> std::path::PathBuf {
self.rust_sysroot().join(RUST_BIN_DIR)
}

pub fn rust_lib(&self) -> std::path::PathBuf {
self.rust_sysroot().join(RUST_LIB_DIR)
}

pub fn command(&self, tool: Tool) -> anyhow::Result<std::process::Command> {
let mut cmd = std::process::Command::new(tool.path(self));
cmd.env_clear();
match tool {
Tool::Cargo | Tool::Rustc => {}
Tool::Charon => {
// The archive supplies Rust tools, but not host build tools
// such as the linker. Keep the caller's `PATH` after our Rust
// bin directory so Cargo builds use the managed Rust toolchain
// while still finding those host tools.
cmd.env("CHARON_TOOLCHAIN_IS_IN_PATH", "1")
.env("PATH", prepend_current_path(self.rust_bin())?)
.env(rust_library_path_env_var(), self.rust_lib());
}
}
Ok(cmd)
}
}

fn prepend_current_path(path: std::path::PathBuf) -> anyhow::Result<std::ffi::OsString> {
let mut paths = vec![path];
if let Some(current_path) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&current_path));
}
std::env::join_paths(paths).context("failed to construct PATH for tool command")
}

/// Returns the platform library search path variable used by Rust tools.
pub(crate) fn rust_library_path_env_var() -> &'static str {
if cfg!(target_os = "macos") { "DYLD_LIBRARY_PATH" } else { "LD_LIBRARY_PATH" }
}

pub fn run_setup(args: SetupArgs) -> anyhow::Result<()> {
let location = resolve_location();
let source = match args.local_archive {
Some(local_archive) => exocrate::Source::Local(local_archive),
None => exocrate::Source::Remote(REMOTE),
};

let (installation_dir, status) = CONFIG
.resolve_installation_dir_or_install(location, source)
Comment thread
joshlf marked this conversation as resolved.
.context("failed to resolve-or-install dependencies")?;
match status {
exocrate::ResolvedOrInstalled::ResolvedExisting => {
log::warn!("anneal toolchain was already installed at {:?}", installation_dir);
}
exocrate::ResolvedOrInstalled::NewlyInstalled => {
log::info!("anneal toolchain freshly installed at {:?}", installation_dir);
}
}
Ok(())
}

fn resolve_location() -> exocrate::Location {
if std::env::var("__ANNEAL_LOCAL_DEV").is_ok() {
exocrate::Location::LocalDev
} else {
exocrate::Location::UserGlobal
}
}
23 changes: 17 additions & 6 deletions exocrate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
//! };
//!
//! // Check whether `source` archive is already installed at `location`, and if not, install it.
//! let installed_exocrate_dir = CONFIG.resolve_installation_dir_or_install(location, source)
//! let (installed_exocrate_dir, _) = CONFIG.resolve_installation_dir_or_install(location, source)
//! .expect("failed to resolve or install my-tool's exocrate");
//!
//! // Invoke tool installed from `tests/my-tool-deps.tar.zst` extracted to versioned exocrate
Expand Down Expand Up @@ -186,6 +186,15 @@ pub enum Source {
Local(PathBuf),
}

/// Whether an installation was resolved or newly installed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ResolvedOrInstalled {
/// An existing installation was resolved.
ResolvedExisting,
/// A new installation was installed.
NewlyInstalled,
}

impl Config {
/// Resolves the dependency directory, failing if it doesn't exist.
pub fn resolve_installation_dir(&self, location: Location) -> IoResult<PathBuf> {
Expand All @@ -199,14 +208,14 @@ impl Config {
&self,
location: Location,
source: Source,
) -> IoResult<PathBuf> {
) -> IoResult<(PathBuf, ResolvedOrInstalled)> {
let dir_path = self.dir_path(location)?;
if ManagedDirName::new(&dir_path).check_exists().is_ok() {
return Ok(dir_path);
return Ok((dir_path, ResolvedOrInstalled::ResolvedExisting));
}
let (reader, expected_sha) = self.open_source(source)?;
install(reader, &dir_path, expected_sha)?;
Ok(dir_path)
Ok((dir_path, ResolvedOrInstalled::NewlyInstalled))
}

/// Opens the given source.
Expand Down Expand Up @@ -766,19 +775,21 @@ mod tests {
let dev_path = config.dir_path(Location::LocalDev).unwrap();
let _ = fs::remove_dir_all(&dev_path);

let resolved = config
let (resolved, status) = config
.resolve_installation_dir_or_install(Location::LocalDev, Source::Local(archive_path))
.unwrap();
assert_eq!(resolved, dev_path);
assert_eq!(status, ResolvedOrInstalled::NewlyInstalled);
assert!(dev_path.join("bin/compiler").exists());

let resolved2 = config
let (resolved2, status2) = config
.resolve_installation_dir_or_install(
Location::LocalDev,
Source::Local(PathBuf::from("/nonexistent/path/should/not/be/accessed")),
)
.unwrap();
assert_eq!(resolved2, dev_path);
assert_eq!(status2, ResolvedOrInstalled::ResolvedExisting);

fs::remove_dir_all(&dev_path).unwrap();
}
Expand Down
Loading