diff --git a/Cargo.toml b/Cargo.toml index 85dad1d..438db87 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", @@ -80,6 +81,7 @@ passthrough = [ "PYAPP_SELF_COMMAND", "PYAPP_SKIP_INSTALL", "PYAPP_UPGRADE_VIRTUALENV", + "PYAPP_UV_CUSTOM_SOURCE", "PYAPP_UV_ENABLED", "PYAPP_UV_ONLY_BOOTSTRAP", "PYAPP_UV_VERSION", diff --git a/build.rs b/build.rs index fdda3d5..ab868b8 100644 --- a/build.rs +++ b/build.rs @@ -310,14 +310,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,6 +944,11 @@ fn set_uv_only_bootstrap() { } } +fn set_uv_custom_source() { + let variable = "PYAPP_UV_CUSTOM_SOURCE"; + set_runtime_variable(variable, env::var(variable).unwrap_or_default()); +} + fn set_uv_version() { let variable = "PYAPP_UV_VERSION"; let version = env::var(variable).unwrap_or("any".to_string()); @@ -1070,6 +1097,7 @@ fn main() { set_pip_allow_config(); set_uv_enabled(); set_uv_only_bootstrap(); + set_uv_custom_source(); set_uv_version(); set_allow_updates(); set_indicator(); 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..24688fb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,7 @@ use anyhow::{Context, Result}; use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; use directories::ProjectDirs; use once_cell::sync::OnceCell; +use reqwest::Url; static PLATFORM_DIRS: OnceCell = OnceCell::new(); static INSTALLATION_DIRECTORY: OnceCell = OnceCell::new(); @@ -206,6 +207,29 @@ pub fn uv_as_installer() -> bool { uv_enabled() && !uv_only_bootstrap() } +pub fn uv_custom_source() -> String { + env!("PYAPP_UV_CUSTOM_SOURCE").into() +} + +pub fn uv_custom_source_artifact_name() -> String { + let custom_source = uv_custom_source(); + + let parsed = + Url::parse(&custom_source).expect(&format!("unable to parse url: {}", &custom_source)); + + // Try to find artifact name from URL path + if let Some(segments) = parsed.path_segments() { + if let Some(segment) = segments.last() { + return segment.into(); + } + } + + panic!( + "unable to determine artifact name from url: {}", + &custom_source + ); +} + pub fn is_gui() -> bool { env!("PYAPP_IS_GUI") == "1" } diff --git a/src/distribution.rs b/src/distribution.rs index 33a9ce3..5543d58 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -488,13 +488,20 @@ 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 = if app::uv_custom_source().is_empty() { + app::uv_artifact_name() + } else { + app::uv_custom_source_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" { + let url = if !app::uv_custom_source().is_empty() { + app::uv_custom_source() + } else if uv_version == "any" { format!( "https://github.com/astral-sh/uv/releases/latest/download/{}", &artifact_name, @@ -505,7 +512,6 @@ fn ensure_uv_available() -> Result<()> { &uv_version, &artifact_name, ) }; - network::download(&url, &mut f, "UV")?; if artifact_name.ends_with(".zip") {