Skip to content

Commit

Permalink
feat: install native packages in CI
Browse files Browse the repository at this point in the history
This injects a new CI step, prior to package build, which installs packages specified
via a new field in Cargo.toml.
  • Loading branch information
mistydemeo committed Sep 27, 2023
1 parent 815ca10 commit 5e28a0f
Show file tree
Hide file tree
Showing 20 changed files with 5,734 additions and 14 deletions.
3 changes: 3 additions & 0 deletions cargo-dist-schema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ pub struct GithubMatrixEntry {
/// Arguments to pass to cargo-dist
#[serde(skip_serializing_if = "Option::is_none")]
pub dist_args: Option<String>,
/// Command to run to install dependencies
#[serde(skip_serializing_if = "Option::is_none")]
pub packages_install: Option<String>,
}

/// Type of job to run on pull request
Expand Down
7 changes: 7 additions & 0 deletions cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ expression: json_schema
"null"
]
},
"packages_install": {
"description": "Command to run to install dependencies",
"type": [
"string",
"null"
]
},
"runner": {
"description": "Github Runner to user",
"type": [
Expand Down
96 changes: 94 additions & 2 deletions cargo-dist/src/backend/ci/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tracing::warn;

use crate::{
backend::{diff_files, templates::TEMPLATE_CI_GITHUB},
config::ProductionMode,
config::{ProductionMode, SystemDependencies},
errors::DistResult,
DistGraph, SortedMap, SortedSet, TargetTriple,
};
Expand Down Expand Up @@ -87,6 +87,7 @@ impl GithubCiInfo {
runner: Some(GITHUB_LINUX_RUNNER.into()),
dist_args: Some("--artifacts=global".into()),
install_dist: Some(install_dist_sh.clone()),
packages_install: None,
})
} else {
None
Expand All @@ -109,13 +110,14 @@ impl GithubCiInfo {
let install_dist =
install_dist_for_github_runner(runner, &install_dist_sh, &install_dist_ps1);
let mut dist_args = String::from("--artifacts=local");
for target in targets {
for target in &targets {
write!(dist_args, " --target={target}").unwrap();
}
tasks.push(GithubMatrixEntry {
runner: Some(runner.to_owned()),
dist_args: Some(dist_args),
install_dist: Some(install_dist.to_owned()),
packages_install: package_install_for_targets(&targets, &dist.system_dependencies),
});
}

Expand Down Expand Up @@ -256,3 +258,93 @@ fn install_dist_for_github_runner<'a>(
unreachable!("internal error: unknown github runner!?")
}
}

fn brewfile_from(packages: &[String]) -> String {
let brewfile_lines: Vec<String> = packages
.iter()
.map(|p| format!(r#"brew "{p}""#).to_owned())
.collect();

brewfile_lines.join("\n")
}

fn brew_bundle_command(packages: &[String]) -> String {
format!(
r#"cat << EOF >Brewfile
{}
EOF
brew bundle install"#,
brewfile_from(packages)
)
}

fn package_install_for_targets(
targets: &Vec<&TargetTriple>,
packages: &SystemDependencies,
) -> Option<String> {
// TODO handle mixed-OS targets
for target in targets {
match target.as_str() {
"i686-apple-darwin" | "x86_64-apple-darwin" | "aarch64-apple-darwin" => {
if packages.homebrew.is_empty() {
return None;
}

let packages: Vec<String> = packages
.homebrew
.clone()
.into_iter()
.filter(|(_, package)| package.0.wanted_for_target(target))
.map(|(name, _)| name)
.collect();
return Some(brew_bundle_command(&packages));
}
"i686-unknown-linux-gnu" | "x86_64-unknown-linux-gnu" | "aarch64-unknown-linux-gnu" => {
if packages.apt.is_empty() {
return None;
}

let apts: String = packages
.apt
.clone()
.into_iter()
.filter(|(_, package)| package.0.wanted_for_target(target))
.map(|(name, spec)| {
if let Some(version) = spec.0.version {
format!("{name}={version}")
} else {
name
}
})
.collect::<Vec<String>>()
.join(" ");
return Some(format!("sudo apt-get install {apts}").to_owned());
}
"i686-pc-windows-msvc" | "x86_64-pc-windows-msvc" | "aarch64-pc-windows-msvc" => {
if packages.chocolatey.is_empty() {
return None;
}

let commands: Vec<String> = packages
.chocolatey
.clone()
.into_iter()
.filter(|(_, package)| package.0.wanted_for_target(target))
.map(|(name, package)| {
if let Some(version) = package.0.version {
format!("choco install {name} --version={version}")
} else {
format!("choco install {name}")
}
})
.collect();

return Some(commands.join("\n"));
}
_ => {}
}
}

None
}
2 changes: 2 additions & 0 deletions cargo-dist/src/backend/installer/homebrew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub struct HomebrewInstallerInfo {
pub arm64_sha256: Option<String>,
/// Generic installer info
pub inner: InstallerInfo,
/// Additional packages to specify as dependencies
pub dependencies: Vec<String>,
}

pub(crate) fn write_homebrew_formula(
Expand Down
135 changes: 135 additions & 0 deletions cargo-dist/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Config types (for workspace.metadata.dist)
use std::collections::BTreeMap;

use axoproject::WorkspaceSearch;
use camino::{Utf8Path, Utf8PathBuf};
use miette::Report;
Expand Down Expand Up @@ -88,6 +90,11 @@ pub struct DistMetadata {
/// A Homebrew tap to push the Homebrew formula to, if built
pub tap: Option<String>,

/// A set of packages to install before building
#[serde(rename = "dependencies")]
#[serde(skip_serializing_if = "Option::is_none")]
pub system_dependencies: Option<SystemDependencies>,

/// The full set of target triples to build for.
///
/// When generating full task graphs (such as CI scripts) we will to try to generate these.
Expand Down Expand Up @@ -278,6 +285,7 @@ impl DistMetadata {
ci: _,
installers: _,
tap: _,
system_dependencies: _,
targets: _,
include,
auto_includes: _,
Expand Down Expand Up @@ -320,6 +328,7 @@ impl DistMetadata {
ci,
installers,
tap,
system_dependencies,
targets,
include,
auto_includes,
Expand Down Expand Up @@ -419,6 +428,9 @@ impl DistMetadata {
if tap.is_none() {
*tap = workspace_config.tap.clone();
}
if system_dependencies.is_none() {
*system_dependencies = workspace_config.system_dependencies.clone();
}
if publish_jobs.is_none() {
*publish_jobs = workspace_config.publish_jobs.clone();
}
Expand Down Expand Up @@ -814,6 +826,129 @@ impl std::fmt::Display for GenerateMode {
}
}

/// Packages to install before build from the system package manager
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SystemDependencies {
/// Packages to install in Homebrew
#[serde(default = "BTreeMap::new")]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
// #[serde(with = "sysdep_derive")]
pub homebrew: BTreeMap<String, SystemDependency>,
/// Packages to install in apt
#[serde(default = "BTreeMap::new")]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub apt: BTreeMap<String, SystemDependency>,
/// Package to install in Chocolatey
#[serde(default = "BTreeMap::new")]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub chocolatey: BTreeMap<String, SystemDependency>,
}

/// Represents a package from a system package manager
// newtype wrapper to hang a manual derive impl off of
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct SystemDependency(pub SystemDependencyComplex);

/// Backing type for SystemDependency
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct SystemDependencyComplex {
/// The version to install, as expected by the underlying package manager
pub version: Option<String>,
/// Phases at which the dependency is required
pub phase: Vec<DependencyKind>,
/// One or more targets this package should be installed on; defaults to all targets if not specified
pub targets: Vec<String>,
}

impl SystemDependencyComplex {
/// Checks if this dependency should be installed on the specified target.
pub fn wanted_for_target(&self, target: &String) -> bool {
if self.targets.is_empty() {
true
} else {
self.targets.contains(target)
}
}
}

/// Definition for a single package
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemDependencyKind {
/// Simple specification format, parsed as cmake = 'version'
/// The special string "*" is parsed as a None version
Untagged(String),
/// Complex specification format
Tagged {
/// Version specification, as expected by the underlying package manager
version: Option<String>,
/// Phases at which the dependency is required
#[serde(default = "DependencyKind::default_phases")]
phase: Vec<DependencyKind>,
/// One or more targets this package should be installed on; defaults to all targets if not specified
#[serde(default = "Vec::new")]
targets: Vec<String>,
},
}

/// Provides detail on when a specific dependency is required
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum DependencyKind {
/// A dependency that must be present when the software is being built
#[serde(rename = "build")]
Build,
/// A dependency that must be present when the software is being used
#[serde(rename = "run")]
Run,
}

impl std::fmt::Display for DependencyKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DependencyKind::Build => "build".fmt(f),
DependencyKind::Run => "run".fmt(f),
}
}
}

impl DependencyKind {
/// Returns the default phases used for dependencies with nothing specified
pub fn default_phases() -> Vec<Self> {
vec![Self::Build]
}
}

impl<'de> Deserialize<'de> for SystemDependency {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let kind: SystemDependencyKind = SystemDependencyKind::deserialize(deserializer)?;

let res = match kind {
SystemDependencyKind::Untagged(version) => {
let v = if version == "*" { None } else { Some(version) };
SystemDependencyComplex {
version: v,
phase: DependencyKind::default_phases(),
targets: vec![],
}
}
SystemDependencyKind::Tagged {
version,
phase,
targets,
} => SystemDependencyComplex {
version,
phase,
targets,
},
};

Ok(SystemDependency(res))
}
}

/// Settings for which Generate targets can be dirty
#[derive(Debug, Clone)]
pub enum DirtyMode {
Expand Down
2 changes: 2 additions & 0 deletions cargo-dist/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ fn get_new_dist_metadata(
ci: None,
installers: None,
tap: None,
system_dependencies: None,
targets: None,
dist: None,
include: None,
Expand Down Expand Up @@ -718,6 +719,7 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
ci,
installers,
tap,
system_dependencies: _,
targets,
include,
auto_includes,
Expand Down
Loading

0 comments on commit 5e28a0f

Please sign in to comment.