diff --git a/Cargo.toml b/Cargo.toml index 85dad1d..2ea7842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ passthrough = [ "PYAPP_DISTRIBUTION_PYTHON_PATH", "PYAPP_DISTRIBUTION_SITE_PACKAGES_PATH", "PYAPP_DISTRIBUTION_SOURCE", + "PYAPP_DISTRIBUTION_SOURCE_{}", "PYAPP_DISTRIBUTION_VARIANT", "PYAPP_EXEC_CODE", "PYAPP_EXEC_MODULE", @@ -82,5 +83,6 @@ passthrough = [ "PYAPP_UPGRADE_VIRTUALENV", "PYAPP_UV_ENABLED", "PYAPP_UV_ONLY_BOOTSTRAP", + "PYAPP_UV_SOURCE", "PYAPP_UV_VERSION", ] diff --git a/build.rs b/build.rs index fdda3d5..0d249df 100644 --- a/build.rs +++ b/build.rs @@ -239,6 +239,19 @@ fn check_environment_variable(name: &str) -> String { value } +fn filename_from_url(url: &str) -> String { + let parsed = + reqwest::Url::parse(url).unwrap_or_else(|_| panic!("unable to parse URL: {}", &url)); + + if let Some(segments) = parsed.path_segments() { + if let Some(segment) = segments.last() { + return segment.into(); + } + } + + panic!("unable to determine artifact name from URL: {}", &url); +} + fn is_enabled(name: &str) -> bool { ["true", "1"].contains(&env::var(name).unwrap_or_default().as_str()) } @@ -310,14 +323,36 @@ fn get_python_version() -> String { DEFAULT_PYTHON_VERSION.to_string() } +fn get_custom_source(name: &str) -> Option { + let name = name.to_uppercase().replace(".", "_"); + let variable_name = format!("PYAPP_DISTRIBUTION_SOURCE_{}", name); + if let Ok(value) = env::var(variable_name) { + if !value.is_empty() { + return Some(value); + } + } + None +} + fn get_distribution_source() -> String { + let selected_python_version = get_python_version(); + + // Return custom source if specified for this version + if let Some(custom_source) = get_custom_source(&selected_python_version) { + dbg!( + "Using custom source for version {}: {}", + &selected_python_version, + &custom_source + ); + return custom_source; + } + + // Otherwise, check if there is a global custom source let distribution_source = env::var("PYAPP_DISTRIBUTION_SOURCE").unwrap_or_default(); if !distribution_source.is_empty() { return distribution_source; }; - let selected_python_version = get_python_version(); - // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts let selected_platform = match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { "windows" => "windows", @@ -922,27 +957,51 @@ fn set_uv_only_bootstrap() { } } -fn set_uv_version() { - let variable = "PYAPP_UV_VERSION"; - let version = env::var(variable).unwrap_or("any".to_string()); - set_runtime_variable(variable, version); +fn set_uv_source() { + let source_variable = "PYAPP_UV_SOURCE"; + let mut source = env::var(source_variable).unwrap_or_default(); - let artifact_name = if !is_enabled("PYAPP_UV_ENABLED") { - "".to_string() - } else if env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" { - // Force MinGW-w64 to use msvc - if env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "gnu" { + if !source.is_empty() { + set_runtime_variable("PYAPP__UV_ARTIFACT_NAME", filename_from_url(&source)); + + let mut hasher = PortableHash::default(); + source.hash(&mut hasher); + set_runtime_variable("PYAPP__UV_VERSION", hasher.finish()); + } else { + let version_variable = "PYAPP_UV_VERSION"; + let version = env::var(version_variable).unwrap_or("any".to_string()); + + let artifact_name = if !is_enabled("PYAPP_UV_ENABLED") { + "".to_string() + } else if env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" { + // Force MinGW-w64 to use msvc + if env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "gnu" { + format!( + "uv-{}-pc-windows-msvc.zip", + env::var("CARGO_CFG_TARGET_ARCH").unwrap() + ) + } else { + format!("uv-{}.zip", env::var("TARGET").unwrap()) + } + } else { + format!("uv-{}.tar.gz", env::var("TARGET").unwrap()) + }; + + source = if version == "any" { format!( - "uv-{}-pc-windows-msvc.zip", - env::var("CARGO_CFG_TARGET_ARCH").unwrap() + "https://github.com/astral-sh/uv/releases/latest/download/{}", + &artifact_name, ) } else { - format!("uv-{}.zip", env::var("TARGET").unwrap()) - } - } else { - format!("uv-{}.tar.gz", env::var("TARGET").unwrap()) - }; - set_runtime_variable("PYAPP__UV_ARTIFACT_NAME", artifact_name); + format!( + "https://github.com/astral-sh/uv/releases/download/{}/{}", + &version, &artifact_name, + ) + }; + set_runtime_variable("PYAPP__UV_ARTIFACT_NAME", artifact_name); + set_runtime_variable("PYAPP__UV_VERSION", &version); + } + set_runtime_variable(source_variable, &source); } fn set_skip_install() { @@ -1070,7 +1129,7 @@ fn main() { set_pip_allow_config(); set_uv_enabled(); set_uv_only_bootstrap(); - set_uv_version(); + set_uv_source(); set_allow_updates(); set_indicator(); set_self_command(); diff --git a/docs/config/distribution.md b/docs/config/distribution.md index 8240b6c..cc84038 100644 --- a/docs/config/distribution.md +++ b/docs/config/distribution.md @@ -41,6 +41,12 @@ You may explicitly set the `PYAPP_DISTRIBUTION_SOURCE` option which overrides th Setting this manually may require you to define extra metadata about the distribution that is required for correct [runtime behavior](../runtime.md). +### Version + +For greater granularity, you may set the `PYAPP_DISTRIBUTION_SOURCE_` option. The source is also the URL to the distribution's archive. + +The placeholder `` is the uppercased version of the distribution name with periods replaced by underscores e.g. `pypy3.10` would become `PYPY3_10`. + ### Format The following formats are supported for the `PYAPP_DISTRIBUTION_FORMAT` option, with the default chosen based on the ending of the source URL: diff --git a/docs/config/installation.md b/docs/config/installation.md index 2957a93..33eca60 100644 --- a/docs/config/installation.md +++ b/docs/config/installation.md @@ -14,6 +14,12 @@ You may use a specific `X.Y.Z` version by setting the `PYAPP_UV_VERSION` option. By default, a version of UV that has already been downloaded by a PyApp application is used. If UV has not yet been downloaded then the latest version is used. +### Source + +You may explicitly set the `PYAPP_UV_SOURCE` option in order to download your own UV release archive. + +The value must end with the archive's real file extension, which is used to determine the extraction method. + ### Only bootstrap You may set the `PYAPP_UV_ONLY_BOOTSTRAP` option to `true` or `1` to only use UV for virtual environment creation and continue using pip for project installation. diff --git a/docs/users.md b/docs/users.md index 74ae42b..cbef1b8 100644 --- a/docs/users.md +++ b/docs/users.md @@ -14,3 +14,7 @@ The following is not intended to be a complete enumeration. Be sure to view the | [Litestar](https://github.com/litestar-org/litestar-fullstack/blob/dc72eee78173790c3e91b0c095ac9e70ba91bedd/scripts/post-builds.py) | [Preservation Workbench](https://github.com/Preservation-Workbench/PWCode/blob/e7777806be35bd60ca8c33e677ffd77e38b277d0/build/make.sh) | [tidal-wave](https://github.com/ebb-earl-co/tidal-wave/blob/6358ede21adb715a053b1e6cc73968933c3bed05/BUILDME.md#pyapp-created-binaries) + +## Industry + +- [Amadeus](https://amadeus.com) \[[1](https://github.com/ofek/pyapp/pull/147)|[2](https://github.com/AmadeusITGroup/pyapp)\] diff --git a/src/app.rs b/src/app.rs index 4cca7ad..a7dde3d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -195,7 +195,11 @@ pub fn uv_only_bootstrap() -> bool { } pub fn uv_version() -> String { - env!("PYAPP_UV_VERSION").into() + env!("PYAPP__UV_VERSION").into() +} + +pub fn uv_source() -> String { + env!("PYAPP_UV_SOURCE").into() } pub fn uv_artifact_name() -> String { diff --git a/src/distribution.rs b/src/distribution.rs index 33a9ce3..2ea840a 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -488,25 +488,13 @@ fn ensure_uv_available() -> Result<()> { .with_context(|| format!("unable to create UV cache {}", &managed_uv_cache.display()))?; let dir = tempdir().with_context(|| "unable to create temporary directory")?; - let artifact_name = app::uv_artifact_name(); + let artifact_name: String = app::uv_artifact_name(); let temp_path = dir.path().join(&artifact_name); let mut f = fs::File::create(&temp_path) .with_context(|| format!("unable to create temporary file: {}", &temp_path.display()))?; - let url = if uv_version == "any" { - format!( - "https://github.com/astral-sh/uv/releases/latest/download/{}", - &artifact_name, - ) - } else { - format!( - "https://github.com/astral-sh/uv/releases/download/{}/{}", - &uv_version, &artifact_name, - ) - }; - - network::download(&url, &mut f, "UV")?; + network::download(&app::uv_source(), &mut f, "UV")?; if artifact_name.ends_with(".zip") { compression::unpack_zip(temp_path, dir.path(), "Unpacking UV".to_string())