From 5cc156e39eba91f78e4b18d3123a59625e698000 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 17 Apr 2024 22:35:14 -0400 Subject: [PATCH] Add target --- crates/uv/src/cli.rs | 9 ++ crates/uv/src/commands/pip_compile.rs | 39 ++++-- crates/uv/src/main.rs | 2 + crates/uv/src/settings.rs | 4 + crates/uv/src/target.rs | 190 ++++++++++++++++++++++++++ crates/uv/tests/pip_compile.rs | 67 +++++++++ 6 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 crates/uv/src/target.rs diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 06020a63524f7..7782801d0f2c1 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -16,6 +16,7 @@ use uv_toolchain::PythonVersion; use crate::commands::{extra_name_with_clap_error, ListFormat, VersionFormat}; use crate::compat; +use crate::target::TargetTriple; #[derive(Parser)] #[command(author, version, long_version = crate::version::version(), about)] @@ -520,6 +521,14 @@ pub(crate) struct PipCompileArgs { #[arg(long, short)] pub(crate) python_version: Option, + /// The platform for which requirements should be resolved. + /// + /// Represented as a "target triple", a string that describes the target platform in terms of + /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or + /// `aaarch64-apple-darwin`. + #[arg(long)] + pub(crate) platform: Option, + /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index c35d216a38a13..d497c0f43a718 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -46,6 +46,7 @@ use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; use crate::commands::{elapsed, ExitStatus}; use crate::printer::Printer; +use crate::target::TargetTriple; /// Resolve a set of requirements into a set of pinned versions. #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] @@ -78,6 +79,7 @@ pub(crate) async fn pip_compile( no_build_isolation: bool, no_build: NoBuild, python_version: Option, + target: Option, exclude_newer: Option, annotation_style: AnnotationStyle, link_mode: LinkMode, @@ -193,21 +195,40 @@ pub(crate) async fn pip_compile( }; // Determine the tags, markers, and interpreter to use for resolution. - let tags = if let Some(python_version) = python_version.as_ref() { - Cow::Owned(Tags::from_env( + let tags = match (target, python_version.as_ref()) { + (Some(target), Some(python_version)) => Cow::Owned(Tags::from_env( + &target.platform(), + (python_version.major(), python_version.minor()), + interpreter.implementation_name(), + interpreter.implementation_tuple(), + interpreter.gil_disabled(), + )?), + (Some(target), None) => Cow::Owned(Tags::from_env( + &target.platform(), + interpreter.python_tuple(), + interpreter.implementation_name(), + interpreter.implementation_tuple(), + interpreter.gil_disabled(), + )?), + (None, Some(python_version)) => Cow::Owned(Tags::from_env( interpreter.platform(), (python_version.major(), python_version.minor()), interpreter.implementation_name(), interpreter.implementation_tuple(), interpreter.gil_disabled(), - )?) - } else { - Cow::Borrowed(interpreter.tags()?) + )?), + (None, None) => Cow::Borrowed(interpreter.tags()?), + }; + + // Apply the platform tags to the markers. + let markers = match (target, python_version) { + (Some(target), Some(python_version)) => { + Cow::Owned(python_version.markers(&target.markers(interpreter.markers()))) + } + (Some(target), None) => Cow::Owned(target.markers(interpreter.markers())), + (None, Some(python_version)) => Cow::Owned(python_version.markers(interpreter.markers())), + (None, None) => Cow::Borrowed(interpreter.markers()), }; - let markers = python_version.map_or_else( - || Cow::Borrowed(interpreter.markers()), - |python_version| Cow::Owned(python_version.markers(interpreter.markers())), - ); // Generate, but don't enforce hashes for the requirements. let hasher = if generate_hashes { diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 4e285ceea2557..7734e664928ed 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -50,6 +50,7 @@ mod logging; mod printer; mod settings; mod shell; +mod target; mod version; #[instrument] @@ -254,6 +255,7 @@ async fn run() -> Result { args.shared.no_build_isolation, no_build, args.shared.python_version, + args.platform, args.shared.exclude_newer, args.shared.annotation_style, args.shared.link_mode, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index d1f1de0a05327..2944050ec96db 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -14,6 +14,7 @@ use crate::cli::{ PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, VenvArgs, }; use crate::commands::ListFormat; +use crate::target::TargetTriple; /// The resolved global settings to use for any invocation of the CLI. #[allow(clippy::struct_excessive_bools)] @@ -74,6 +75,7 @@ pub(crate) struct PipCompileSettings { pub(crate) src_file: Vec, pub(crate) constraint: Vec, pub(crate) r#override: Vec, + pub(crate) platform: Option, pub(crate) refresh: bool, pub(crate) refresh_package: Vec, pub(crate) upgrade: bool, @@ -134,6 +136,7 @@ impl PipCompileSettings { only_binary, config_setting, python_version, + platform, exclude_newer, no_emit_package, emit_index_url, @@ -152,6 +155,7 @@ impl PipCompileSettings { src_file, constraint, r#override, + platform, refresh, refresh_package: refresh_package.unwrap_or_default(), upgrade, diff --git a/crates/uv/src/target.rs b/crates/uv/src/target.rs new file mode 100644 index 0000000000000..70a279e361bc6 --- /dev/null +++ b/crates/uv/src/target.rs @@ -0,0 +1,190 @@ +use pep508_rs::MarkerEnvironment; +use platform_tags::{Arch, Os, Platform}; + +/// The supported target triples. Each triple consists of an architecture, vendor, and operating +/// system. +/// +/// See: +#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)] +pub(crate) enum TargetTriple { + /// An alias for `x86_64-pc-windows-msvc`, the default target for Windows. + Windows, + + /// An alias for `x86_64-unknown-linux-gnu`, the default target for Linux. + Linux, + + /// An alias for `aarch64-apple-darwin`, the default target for macOS. + Macos, + + /// An x86 Windows target. + #[value(name = "x86_64-pc-windows-msvc")] + X8664PcWindowsMsvc, + + /// An x86 Linux target. + #[value(name = "x86_64-unknown-linux-gnu")] + X8664UnknownLinuxGnu, + + /// An ARM-based macOS target, as seen on Apple Silicon devices. + #[value(name = "aarch64-apple-darwin")] + Aarch64AppleDarwin, + + /// An x86 macOS target. + #[value(name = "x86_64-apple-darwin")] + X8664AppleDarwin, + + /// An ARM64 Linux target. + #[value(name = "aarch64-unknown-linux-gnu")] + Aarch64UnknownLinuxGnu, + + /// An ARM64 Linux target. + #[value(name = "aarch64-unknown-linux-musl")] + Aarch64UnknownLinuxMusl, + + /// An x86_64 Linux target. + #[value(name = "x86_64-unknown-linux-musl")] + X8664UnknownLinuxMusl, +} + +impl TargetTriple { + /// Return the [`Platform`] for the target. + pub(crate) fn platform(self) -> Platform { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => Platform::new(Os::Windows, Arch::X86_64), + Self::Linux | Self::X8664UnknownLinuxGnu => Platform::new( + Os::Manylinux { + major: 2, + minor: 17, + }, + Arch::X86_64, + ), + Self::Macos | Self::Aarch64AppleDarwin => Platform::new( + Os::Macos { + major: 11, + minor: 0, + }, + Arch::Aarch64, + ), + Self::X8664AppleDarwin => Platform::new( + Os::Macos { + major: 10, + minor: 12, + }, + Arch::X86_64, + ), + Self::Aarch64UnknownLinuxGnu => Platform::new( + Os::Manylinux { + major: 2, + minor: 17, + }, + Arch::Aarch64, + ), + Self::Aarch64UnknownLinuxMusl => { + Platform::new(Os::Musllinux { major: 1, minor: 2 }, Arch::Aarch64) + } + Self::X8664UnknownLinuxMusl => { + Platform::new(Os::Musllinux { major: 1, minor: 2 }, Arch::X86_64) + } + } + } + + /// Return the `platform_machine` value for the target. + pub(crate) fn platform_machine(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "x86_64", + Self::Linux | Self::X8664UnknownLinuxGnu => "x86_64", + Self::Macos | Self::Aarch64AppleDarwin => "aarch64", + Self::X8664AppleDarwin => "x86_64", + Self::Aarch64UnknownLinuxGnu => "aarch64", + Self::Aarch64UnknownLinuxMusl => "aarch64", + Self::X8664UnknownLinuxMusl => "x86_64", + } + } + + /// Return the `platform_system` value for the target. + pub(crate) fn platform_system(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "Windows", + Self::Linux | Self::X8664UnknownLinuxGnu => "Linux", + Self::Macos | Self::Aarch64AppleDarwin => "Darwin", + Self::X8664AppleDarwin => "Darwin", + Self::Aarch64UnknownLinuxGnu => "Linux", + Self::Aarch64UnknownLinuxMusl => "Linux", + Self::X8664UnknownLinuxMusl => "Linux", + } + } + + /// Return the `platform_version` value for the target. + pub(crate) fn platform_version(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "", + Self::Linux | Self::X8664UnknownLinuxGnu => "", + Self::Macos | Self::Aarch64AppleDarwin => "", + Self::X8664AppleDarwin => "", + Self::Aarch64UnknownLinuxGnu => "", + Self::Aarch64UnknownLinuxMusl => "", + Self::X8664UnknownLinuxMusl => "", + } + } + + /// Return the `platform_release` value for the target. + pub(crate) fn platform_release(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "", + Self::Linux | Self::X8664UnknownLinuxGnu => "", + Self::Macos | Self::Aarch64AppleDarwin => "", + Self::X8664AppleDarwin => "", + Self::Aarch64UnknownLinuxGnu => "", + Self::Aarch64UnknownLinuxMusl => "", + Self::X8664UnknownLinuxMusl => "", + } + } + + /// Return the `os_name` value for the target. + pub(crate) fn os_name(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "nt", + Self::Linux | Self::X8664UnknownLinuxGnu => "posix", + Self::Macos | Self::Aarch64AppleDarwin => "posix", + Self::X8664AppleDarwin => "posix", + Self::Aarch64UnknownLinuxGnu => "posix", + Self::Aarch64UnknownLinuxMusl => "posix", + Self::X8664UnknownLinuxMusl => "posix", + } + } + + /// Return the `sys_platform` value for the target. + pub(crate) fn sys_platform(self) -> &'static str { + match self { + Self::Windows | Self::X8664PcWindowsMsvc => "win32", + Self::Linux | Self::X8664UnknownLinuxGnu => "linux", + Self::Macos | Self::Aarch64AppleDarwin => "darwin", + Self::X8664AppleDarwin => "darwin", + Self::Aarch64UnknownLinuxGnu => "linux", + Self::Aarch64UnknownLinuxMusl => "linux", + Self::X8664UnknownLinuxMusl => "linux", + } + } + + /// Return a [`MarkerEnvironment`] compatible with the given [`TargetTriple`], based on + /// a base [`MarkerEnvironment`]. + /// + /// The returned [`MarkerEnvironment`] will preserve the base environment's Python version + /// markers, but override its platform markers. + pub(crate) fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment { + MarkerEnvironment { + // Platform markers + os_name: self.os_name().to_string(), + platform_machine: self.platform_machine().to_string(), + platform_system: self.platform_system().to_string(), + sys_platform: self.sys_platform().to_string(), + platform_release: self.platform_release().to_string(), + platform_version: self.platform_version().to_string(), + // Python version markers + implementation_name: base.implementation_name.clone(), + implementation_version: base.implementation_version.clone(), + platform_python_implementation: base.platform_python_implementation.clone(), + python_full_version: base.python_full_version.clone(), + python_version: base.python_version.clone(), + } + } +} diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index d0929d8ba9c74..2f6e467ee61ae 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -7890,6 +7890,73 @@ fn no_version_for_direct_dependency() -> Result<()> { Ok(()) } +/// Compile against a dedicated platform, which may differ from the current platform. +#[test] +fn platform() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("black")?; + + uv_snapshot!( context.filters(), + windows_filters=false, context.compile() + .arg("requirements.in") + .arg("--platform") + .arg("aarch64-unknown-linux-gnu"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --platform aarch64-unknown-linux-gnu + black==24.3.0 + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + uv_snapshot!( context.filters(), + windows_filters=false, context.compile() + .arg("requirements.in") + .arg("--platform") + .arg("x86_64-pc-windows-msvc"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --platform x86_64-pc-windows-msvc + black==24.3.0 + click==8.1.7 + # via black + colorama==0.4.6 + # via click + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 7 packages in [TIME] + "### + ); + + Ok(()) +} + /// Verify that command-line arguments take precedence over on-disk configuration. #[test] fn resolve_configuration() -> Result<()> {