Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

separate run and rustfix modes into their own modules #214

Merged
merged 17 commits into from
Apr 7, 2024
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `Config::custom_comments`
* `Revisioned::custom`
* `Flag` trait for custom `//@` flags
* `Build` trait for custom aux/dep build
* `BuildManager` for deduplicating these builds on a per-`Config` basis

### Fixed

Expand All @@ -22,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* replaced `Mode::Run` with a rustc-specific run flag
* replaced rustfix with a rustc-specific rustfix flag
* replaced `rustfix` fields of `Mode::Fail` and `Mode::Yolo` by instead overwriting the rustc-specific custom flag
* aux builds and dependencies are now built *per* `Config` instead of being built just for the first `Config` and the result shared by the others
* the configs could be different enough that aux builds built with a different config are incompatible (e.g. different targets).
* replaced `Revisioned::aux_builds` with a rustc-specific custom flag

### Removed

Expand Down
160 changes: 160 additions & 0 deletions src/aux_builds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Everything needed to build auxilary files with rustc
// lol we can't name this file `aux.rs` on windows

use bstr::ByteSlice;
use spanned::Spanned;
use std::{ffi::OsString, path::PathBuf, process::Command};

use crate::{
build_manager::{Build, BuildManager},
custom_flags::Flag,
default_per_file_config,
per_test_config::{Comments, TestConfig},
rustc_stderr, CrateType, Error, Errored,
};

impl Flag for AuxBuilder {
fn clone_inner(&self) -> Box<dyn Flag> {
Box::new(self.clone())
}
fn apply(
&self,
cmd: &mut Command,
config: &TestConfig<'_>,
build_manager: &BuildManager<'_>,
) -> Result<(), Errored> {
let aux = &self.aux_file;
let aux_dir = config.aux_dir;
let line = aux.line();
let aux_file = if aux.starts_with("..") {
aux_dir.parent().unwrap().join(&aux.content)
} else {
aux_dir.join(&aux.content)
};
let extra_args = build_manager
.build(AuxBuilder {
aux_file: Spanned::new(
crate::core::strip_path_prefix(
&aux_file.canonicalize().map_err(|err| Errored {
command: Command::new(format!(
"canonicalizing path `{}`",
aux_file.display()
)),
errors: vec![],
stderr: err.to_string().into_bytes(),
stdout: vec![],
})?,
&std::env::current_dir().unwrap(),
)
.collect(),
aux.span(),
),
})
.map_err(
|Errored {
command,
errors,
stderr,
stdout,
}| Errored {
command,
errors: vec![Error::Aux {
path: aux_file.to_path_buf(),
errors,
line,
}],
stderr,
stdout,
},
)?;
cmd.args(extra_args);
Ok(())
}
}

/// Build an aux-build.
/// Custom `//@aux-build` flag handler.
#[derive(Clone, Debug)]
pub struct AuxBuilder {
/// Full path to the file (including `auxiliary` folder prefix)
pub aux_file: Spanned<PathBuf>,
}

impl Build for AuxBuilder {
fn build(&self, build_manager: &BuildManager<'_>) -> Result<Vec<OsString>, Errored> {
let mut config = build_manager.config().clone();
let file_contents = std::fs::read(&self.aux_file.content).map_err(|err| Errored {
command: Command::new(format!("reading aux file `{}`", self.aux_file.display())),
errors: vec![],
stderr: err.to_string().into_bytes(),
stdout: vec![],
})?;
let comments = Comments::parse(&file_contents, &config, &self.aux_file)
.map_err(|errors| Errored::new(errors, "parse aux comments"))?;
assert_eq!(
comments.revisions, None,
"aux builds cannot specify revisions"
);

default_per_file_config(&mut config, &self.aux_file, &file_contents);

match CrateType::from_file_contents(&file_contents) {
// Proc macros must be run on the host
CrateType::ProcMacro => config.target = config.host.clone(),
CrateType::Test | CrateType::Bin | CrateType::Lib => {}
}

let mut config = TestConfig {
config,
revision: "",
comments: &comments,
path: &self.aux_file,
aux_dir: self.aux_file.parent().unwrap(),
};

config.patch_out_dir();

let mut aux_cmd = config.build_command(build_manager)?;

aux_cmd.arg("--emit=link");
let filename = self.aux_file.file_stem().unwrap().to_str().unwrap();
let output = aux_cmd.output().unwrap();
if !output.status.success() {
let error = Error::Command {
kind: "compilation of aux build failed".to_string(),
status: output.status,
};
return Err(Errored {
command: aux_cmd,
errors: vec![error],
stderr: rustc_stderr::process(&self.aux_file, &output.stderr).rendered,
stdout: output.stdout,
});
}

// Now run the command again to fetch the output filenames
aux_cmd.arg("--print").arg("file-names");
let output = aux_cmd.output().unwrap();
assert!(output.status.success());

let mut extra_args = vec![];
for file in output.stdout.lines() {
let file = std::str::from_utf8(file).unwrap();
let crate_name = filename.replace('-', "_");
let path = config.config.out_dir.join(file);
extra_args.push("--extern".into());
let mut cname = OsString::from(&crate_name);
cname.push("=");
cname.push(path);
extra_args.push(cname);
// Help cargo find the crates added with `--extern`.
extra_args.push("-L".into());
extra_args.push(config.config.out_dir.as_os_str().to_os_string());
}
Ok(extra_args)
}

fn description(&self) -> String {
format!("Building aux file {}", self.aux_file.display())
}
}
116 changes: 116 additions & 0 deletions src/build_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Auxiliary and dependency builder. Extendable to custom builds.

use std::{
collections::{hash_map::Entry, HashMap},
ffi::OsString,
process::Command,
sync::{Arc, OnceLock, RwLock},
};

use crate::{status_emitter::StatusEmitter, Config, Errored};

/// A build shared between all tests of the same `BuildManager`
pub trait Build {
/// Runs the build and returns command line args to add to the test so it can find
/// the built things.
fn build(&self, build_manager: &BuildManager<'_>) -> Result<Vec<OsString>, Errored>;
/// Must uniquely describe the build, as it is used for checking that a value
/// has already been cached.
fn description(&self) -> String;
}

/// Deduplicates builds
pub struct BuildManager<'a> {
#[allow(clippy::type_complexity)]
cache: RwLock<HashMap<String, Arc<OnceLock<Result<Vec<OsString>, ()>>>>>,
status_emitter: &'a dyn StatusEmitter,
config: Config,
}

impl<'a> BuildManager<'a> {
/// Create a new `BuildManager` for a specific `Config`. Each `Config` needs
/// to have its own.
pub fn new(status_emitter: &'a dyn StatusEmitter, config: Config) -> Self {
Self {
cache: Default::default(),
status_emitter,
config,
}
}
/// This function will block until the build is done and then return the arguments
/// that need to be passed in order to build the dependencies.
/// The error is only reported once, all follow up invocations of the same build will
/// have a generic error about a previous build failing.
pub fn build(&self, what: impl Build) -> Result<Vec<OsString>, Errored> {
let description = what.description();
// Fast path without much contention.
if let Some(res) = self
.cache
.read()
.unwrap()
.get(&description)
.and_then(|o| o.get())
{
return res.clone().map_err(|()| Errored {
command: Command::new(format!("{description:?}")),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
});
}
let mut lock = self.cache.write().unwrap();
let once = match lock.entry(description) {
Entry::Occupied(entry) => {
if let Some(res) = entry.get().get() {
return res.clone().map_err(|()| Errored {
command: Command::new(format!("{:?}", what.description())),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
});
}
entry.get().clone()
}
Entry::Vacant(entry) => {
let once = Arc::new(OnceLock::new());
entry.insert(once.clone());
once
}
};
drop(lock);

let mut err = None;
once.get_or_init(|| {
let build = self
.status_emitter
.register_test(what.description().into())
.for_revision("");
let res = what.build(self).map_err(|e| err = Some(e));
build.done(
&res.as_ref()
.map(|_| crate::test_result::TestOk::Ok)
.map_err(|()| Errored {
command: Command::new(what.description()),
errors: vec![],
stderr: vec![],
stdout: vec![],
}),
);
res
})
.clone()
.map_err(|()| {
err.unwrap_or_else(|| Errored {
command: Command::new(what.description()),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
})
})
}

/// The `Config` used for all builds.
pub fn config(&self) -> &Config {
&self.config
}
}
Loading
Loading