diff --git a/.changes/1491.json b/.changes/1491.json new file mode 100644 index 000000000..b0cd7b98e --- /dev/null +++ b/.changes/1491.json @@ -0,0 +1,5 @@ +{ + "description": "Allow specifying only a tag or subtarget for images in config", + "issues": [1169], + "type": "changed" +} diff --git a/Cargo.toml b/Cargo.toml index d87376ffb..e8a10ae4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,12 @@ search = "" replace = "\n\n[Unreleased]: https://github.com/cross-rs/{{crate_name}}/compare/v{{version}}...HEAD" exactly = 1 +[[package.metadata.release.pre-release-replacements]] +file = "docs/config_file.md" +search = "(# Translates to `.*?:).*?(-centos`)" +replace = "${1}{{version}}$2" +exactly = 1 + [package.metadata.binstall] pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.tar.gz" bin-dir = "{ bin }{ binary-ext }" diff --git a/docs/config_file.md b/docs/config_file.md index 2c004720a..be67ee1f2 100644 --- a/docs/config_file.md +++ b/docs/config_file.md @@ -221,6 +221,27 @@ the default one. Normal Docker behavior applies, so: - If only `image:latest` is specified, then Docker won't look in Docker Hub. - If the tag is omitted, then Docker will use the `latest` tag. +If you specify a tag but no image name, `cross` will use the default image with +the tag you provided: + +```toml +[target.aarch64-unknown-linux-gnu] +# Translates to `ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge` +image = ":edge" + +[target.x86_64-unknown-linux-musl] +# Translates to `ghcr.io/cross-rs/x86_64-unknown-linux-musl@sha256:77db671d8356a64ae72a3e1415e63f547f26d374fbe3c4762c1cd36c7eac7b99` +image = "@sha256:77db671d8356a64ae72a3e1415e63f547f26d374fbe3c4762c1cd36c7eac7b99" +``` + +You can also specify a subtarget with no tag nor image name: + +```toml +[target.x86_64-unknown-linux-gnu] +# Translates to `ghcr.io/cross-rs/x86_64-unknown-linux-gnu:0.3.0-centos` +image = "-centos" +``` + The `image` key can also take the toolchains/platforms supported by the image: ```toml diff --git a/src/bin/commands/run.rs b/src/bin/commands/run.rs index acd6fd9a0..836a90bd0 100644 --- a/src/bin/commands/run.rs +++ b/src/bin/commands/run.rs @@ -76,7 +76,7 @@ impl Run { } }; - let image = image.to_definite_with(&engine, msg_info); + let image = image.to_definite_with(&engine, msg_info)?; let paths = docker::DockerPaths::create(&engine, metadata, cwd, toolchain, msg_info)?; let options = docker::DockerOptions::new( diff --git a/src/config.rs b/src/config.rs index 853745cff..0979ef7dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,10 +41,10 @@ impl PartialEq<(Option, Option)> for ConfVal { } #[derive(Debug)] -struct Environment(&'static str, Option>); +pub(crate) struct Environment(&'static str, Option>); impl Environment { - fn new(map: Option>) -> Self { + pub(crate) fn new(map: Option>) -> Self { Environment("CROSS", map) } @@ -352,7 +352,7 @@ impl Config { } #[cfg(test)] - fn new_with(toml: Option, env: Environment) -> Self { + pub(crate) fn new_with(toml: Option, env: Environment) -> Self { Config { toml, env } } diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 87328d328..40ae74317 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -591,7 +591,7 @@ where #[cfg(test)] mod tests { - use crate::docker::ImagePlatform; + use crate::docker::{ImagePlatform, ImageReference}; use super::*; use crate::shell; @@ -741,7 +741,7 @@ mod tests { build_std: None, zig: None, image: Some(PossibleImage { - name: "test-image".to_owned(), + reference: ImageReference::Name("test-image".to_owned()), toolchain: vec![ImagePlatform::from_target( "aarch64-unknown-linux-musl".into(), )?], @@ -773,7 +773,7 @@ mod tests { enable: None, version: None, image: Some(PossibleImage { - name: "zig:local".to_owned(), + reference: ImageReference::Name("zig:local".to_owned()), toolchain: vec![ImagePlatform::from_target( "aarch64-unknown-linux-gnu".into(), )?], @@ -939,7 +939,7 @@ mod tests { [target.target3] xargo = false build-std = true - image = "test-image3" + image = "@sha256:test-image3" [target.target3.env] volumes = ["VOL3_ARG"] @@ -978,7 +978,7 @@ mod tests { [target.target3] xargo = false build-std = true - image = "test-image3" + image = "@sha256:test-image3" [target.target3.env] volumes = ["VOL3_ARG"] @@ -1042,7 +1042,7 @@ mod tests { let target3 = &targets[&Target::new_custom("target3")]; assert_eq!(target3.build_std, Some(BuildStd::Bool(true))); assert_eq!(target3.xargo, Some(false)); - assert_eq!(target3.image, Some(p!("test-image3"))); + assert_eq!(target3.image, Some(p!("@sha256:test-image3"))); assert_eq!(target3.pre_build, None); assert_eq!(target3.dockerfile, None); assert_eq!(target3.env.passthrough, Some(vec![p!("VAR3")])); diff --git a/src/docker/image.rs b/src/docker/image.rs index 6f45b5d3f..3d3c15bca 100644 --- a/src/docker/image.rs +++ b/src/docker/image.rs @@ -2,7 +2,12 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; -use crate::{errors::*, shell::MessageInfo, TargetTriple}; +use crate::{ + docker::{CROSS_IMAGE, DEFAULT_IMAGE_VERSION}, + errors::*, + shell::MessageInfo, + TargetTriple, +}; use super::Engine; @@ -21,18 +26,23 @@ impl std::fmt::Display for Image { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct PossibleImage { - pub name: String, + #[serde(rename = "name")] + pub reference: ImageReference, // The toolchain triple the image is built for pub toolchain: Vec, } impl PossibleImage { - pub fn to_definite_with(&self, engine: &Engine, msg_info: &mut MessageInfo) -> Image { + pub fn to_definite_with(&self, engine: &Engine, msg_info: &mut MessageInfo) -> Result { + let ImageReference::Name(name) = self.reference.clone() else { + eyre::bail!("cannot make definite Image from unqualified PossibleImage"); + }; + if self.toolchain.is_empty() { - Image { - name: self.name.clone(), + Ok(Image { + name, platform: ImagePlatform::DEFAULT, - } + }) } else { let platform = if self.toolchain.len() == 1 { self.toolchain.first().expect("should contain at least one") @@ -71,10 +81,10 @@ impl PossibleImage { platform } }; - Image { + Ok(Image { platform: platform.clone(), - name: self.name.clone(), - } + name, + }) } } } @@ -82,7 +92,7 @@ impl PossibleImage { impl> From for PossibleImage { fn from(s: T) -> Self { PossibleImage { - name: s.as_ref().to_owned(), + reference: s.as_ref().to_owned().into(), toolchain: vec![], } } @@ -98,9 +108,57 @@ impl FromStr for PossibleImage { impl std::fmt::Display for PossibleImage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.name) + f.write_str(self.reference.get()) } } + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(from = "String", untagged)] +pub enum ImageReference { + /// Partially qualified reference, with or without tag/digest + Name(String), + /// Unqualified reference, only a tag or digest + Identifier(String), + /// Unqualified reference, only a subtarget + Subtarget(String), +} + +impl ImageReference { + pub fn get(&self) -> &str { + match self { + Self::Name(s) => s, + Self::Identifier(s) => s, + Self::Subtarget(s) => s, + } + } + + pub fn ensure_qualified(&mut self, target_name: &str) { + let image_name = match self { + Self::Name(_) => return, + Self::Identifier(id) => { + format!("{CROSS_IMAGE}/{target_name}{id}") + } + Self::Subtarget(sub) => { + format!("{CROSS_IMAGE}/{target_name}:{DEFAULT_IMAGE_VERSION}{sub}") + } + }; + + *self = Self::Name(image_name); + } +} + +impl From for ImageReference { + fn from(s: String) -> Self { + if s.starts_with('-') { + Self::Subtarget(s) + } else if s.starts_with(':') || s.starts_with('@') { + Self::Identifier(s) + } else { + Self::Name(s) + } + } +} + /// The architecture/platform to use in the image /// /// diff --git a/src/docker/mod.rs b/src/docker/mod.rs index f61798aec..def816933 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -12,7 +12,9 @@ pub use self::engine::*; pub use self::provided_images::PROVIDED_IMAGES; pub use self::shared::*; -pub use image::{Architecture, Image, ImagePlatform, Os as ContainerOs, PossibleImage}; +pub use image::{ + Architecture, Image, ImagePlatform, ImageReference, Os as ContainerOs, PossibleImage, +}; use std::process::ExitStatus; @@ -31,6 +33,10 @@ impl ProvidedImage { pub fn image_name(&self, repository: &str, tag: &str) -> String { image_name(self.name, self.sub, repository, tag) } + + pub fn default_image_name(&self) -> String { + self.image_name(CROSS_IMAGE, DEFAULT_IMAGE_VERSION) + } } pub fn image_name(target: &str, sub: Option<&str>, repository: &str, tag: &str) -> String { diff --git a/src/docker/shared.rs b/src/docker/shared.rs index c519d59e6..4a1c56f89 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -5,10 +5,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::{env, fs, time}; use super::custom::{Dockerfile, PreBuild}; -use super::engine::*; use super::image::PossibleImage; use super::Image; use super::PROVIDED_IMAGES; +use super::{engine::*, ProvidedImage}; use crate::cargo::CargoMetadata; use crate::config::Config; use crate::errors::*; @@ -26,6 +26,11 @@ pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX; pub const CROSS_IMAGE: &str = "ghcr.io/cross-rs"; // note: this is the most common base image for our images pub const UBUNTU_BASE: &str = "ubuntu:20.04"; +pub const DEFAULT_IMAGE_VERSION: &str = if crate::commit_info().is_empty() { + env!("CARGO_PKG_VERSION") +} else { + "main" +}; #[derive(Debug)] pub struct DockerOptions { @@ -1228,23 +1233,35 @@ pub enum GetImageError { Other(eyre::Report), } -/// Simpler version of [get_image] -pub fn get_image_name( +fn get_target_name(target: &Target, uses_zig: bool) -> &str { + if uses_zig { + "zig" + } else { + target.triple() + } +} + +fn get_user_image( config: &Config, target: &Target, uses_zig: bool, -) -> Result { - if let Some(image) = config.image(target).map_err(GetImageError::Other)? { - return Ok(image.name); +) -> Result, GetImageError> { + let mut image = config.image(target).map_err(GetImageError::Other)?; + if image.is_none() && uses_zig { + image = config.zig_image(target).map_err(GetImageError::Other)?; } - let target_name = match uses_zig { - true => match config.zig_image(target).map_err(GetImageError::Other)? { - Some(image) => return Ok(image.name), - None => "zig", - }, - false => target.triple(), - }; + if let Some(image) = &mut image { + let target_name = get_target_name(target, uses_zig); + image.reference.ensure_qualified(target_name); + } + + Ok(image) +} + +fn get_provided_images_for_target( + target_name: &str, +) -> Result, GetImageError> { let compatible = PROVIDED_IMAGES .iter() .filter(|p| p.name == target_name) @@ -1254,16 +1271,25 @@ pub fn get_image_name( return Err(GetImageError::NoCompatibleImages(target_name.to_owned())); } - let version = if crate::commit_info().is_empty() { - env!("CARGO_PKG_VERSION") - } else { - "main" - }; + Ok(compatible) +} + +/// Simpler version of [get_image] +pub fn get_image_name( + config: &Config, + target: &Target, + uses_zig: bool, +) -> Result { + if let Some(image) = get_user_image(config, target, uses_zig)? { + return Ok(image.reference.get().to_owned()); + } + let target_name = get_target_name(target, uses_zig); + let compatible = get_provided_images_for_target(target_name)?; Ok(compatible .first() .expect("should not be empty") - .image_name(CROSS_IMAGE, version)) + .default_image_name()) } pub fn get_image( @@ -1271,35 +1297,15 @@ pub fn get_image( target: &Target, uses_zig: bool, ) -> Result { - if let Some(image) = config.image(target).map_err(GetImageError::Other)? { + if let Some(image) = get_user_image(config, target, uses_zig)? { return Ok(image); } - let target_name = match uses_zig { - true => match config.zig_image(target).map_err(GetImageError::Other)? { - Some(image) => return Ok(image), - None => "zig", - }, - false => target.triple(), - }; - let compatible = PROVIDED_IMAGES - .iter() - .filter(|p| p.name == target_name) - .collect::>(); - - if compatible.is_empty() { - return Err(GetImageError::NoCompatibleImages(target_name.to_owned())); - } - - let version = if crate::commit_info().is_empty() { - env!("CARGO_PKG_VERSION") - } else { - "main" - }; - - let pick = if compatible.len() == 1 { + let target_name = get_target_name(target, uses_zig); + let compatible = get_provided_images_for_target(target_name)?; + let pick = if let [first] = compatible[..] { // If only one match, use that - compatible.first().expect("should not be empty") + first } else if compatible .iter() .filter(|provided| provided.sub.is_none()) @@ -1323,10 +1329,7 @@ pub fn get_image( "candidates: {}", compatible .iter() - .map(|provided| format!( - "\"{}\"", - provided.image_name(CROSS_IMAGE, version) - )) + .map(|provided| format!("\"{}\"", provided.default_image_name())) .collect::>() .join(", ") ) @@ -1334,12 +1337,12 @@ pub fn get_image( )); }; - let mut image: PossibleImage = pick.image_name(CROSS_IMAGE, version).into(); - + let image_name = pick.default_image_name(); if pick.platforms.is_empty() { - return Err(GetImageError::SpecifiedImageNoPlatform(image.to_string())); - }; + return Err(GetImageError::SpecifiedImageNoPlatform(image_name)); + } + let mut image: PossibleImage = image_name.into(); image.toolchain = pick.platforms.to_vec(); Ok(image) } @@ -1544,8 +1547,10 @@ pub fn path_hash(path: &Path, count: usize) -> Result { #[cfg(test)] mod tests { + use std::collections::HashMap; + use super::*; - use crate::id; + use crate::{config::Environment, id}; #[cfg(not(target_os = "windows"))] use crate::file::PathExt; @@ -1600,6 +1605,52 @@ mod tests { } } + #[test] + fn test_tag_only_image() -> Result<()> { + let target: Target = TargetTriple::X86_64UnknownLinuxGnu.into(); + let test = |map, expected_ver: &str, expected_ver_zig: &str| -> Result<()> { + let env = Environment::new(Some(map)); + let config = Config::new_with(None, env); + for (uses_zig, expected_ver) in [(false, expected_ver), (true, expected_ver_zig)] { + let expected_image_target = if uses_zig { + "zig" + } else { + "x86_64-unknown-linux-gnu" + }; + let expected = format!("ghcr.io/cross-rs/{expected_image_target}{expected_ver}"); + + let image = get_image(&config, &target, uses_zig)?; + assert_eq!(image.reference.get(), expected); + let image_name = get_image_name(&config, &target, uses_zig)?; + assert_eq!(image_name, expected); + } + Ok(()) + }; + + let default_ver = format!(":{DEFAULT_IMAGE_VERSION}"); + let mut map = HashMap::new(); + test(map.clone(), &default_ver, &default_ver)?; + + map.insert("CROSS_TARGET_X86_64_UNKNOWN_LINUX_GNU_IMAGE", "-centos"); + let centos_tag = format!("{default_ver}-centos"); + test(map.clone(), ¢os_tag, ¢os_tag)?; + + map.insert("CROSS_TARGET_X86_64_UNKNOWN_LINUX_GNU_IMAGE", ":edge"); + test(map.clone(), ":edge", ":edge")?; + + // `image` always takes precedence over `zig.image`, even when `uses_zig` is `true` + map.insert( + "CROSS_TARGET_X86_64_UNKNOWN_LINUX_GNU_ZIG_IMAGE", + "@sha256:foobar", + ); + test(map.clone(), ":edge", ":edge")?; + + map.remove("CROSS_TARGET_X86_64_UNKNOWN_LINUX_GNU_IMAGE"); + test(map.clone(), &default_ver, "@sha256:foobar")?; + + Ok(()) + } + mod directories { use super::*; use crate::cargo::cargo_metadata_with_args; diff --git a/src/lib.rs b/src/lib.rs index 33061ff32..6624586f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -810,7 +810,7 @@ To override the toolchain mounted in the image, set `target.{target}.image.toolc }; let is_remote = docker::Engine::is_remote(); let engine = docker::Engine::new(None, Some(is_remote), msg_info)?; - let image = image.to_definite_with(&engine, msg_info); + let image = image.to_definite_with(&engine, msg_info)?; toolchain.replace_host(&image.platform); Ok(Some(CrossSetup { config,