Skip to content

Commit

Permalink
Add support for UV
Browse files Browse the repository at this point in the history
  • Loading branch information
ofek committed Apr 21, 2024
1 parent eb317a4 commit 6a256fa
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 102 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,7 @@ passthrough = [
"PYAPP_SELF_COMMAND",
"PYAPP_SKIP_INSTALL",
"PYAPP_UPGRADE_VIRTUALENV",
"PYAPP_UV_ENABLED",
"PYAPP_UV_ONLY_BOOTSTRAP",
"PYAPP_UV_VERSION",
]
57 changes: 50 additions & 7 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,12 @@ fn get_distribution_source() -> String {
let selected_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let selected_variant = {
let mut variant = env::var("PYAPP_DISTRIBUTION_VARIANT").unwrap_or_default();
if variant.is_empty() {
if selected_platform == "linux"
&& selected_arch == "x86_64"
&& selected_python_version != "3.7"
{
variant = "v3".to_string();
}
if variant.is_empty()
&& selected_platform == "linux"
&& selected_arch == "x86_64"
&& selected_python_version != "3.7"
{
variant = "v3".to_string();
};
variant
};
Expand Down Expand Up @@ -852,6 +851,47 @@ fn set_pip_allow_config() {
}
}

fn set_uv_enabled() {
let variable = "PYAPP_UV_ENABLED";
if is_enabled(variable) {
set_runtime_variable(variable, "1");
} else {
set_runtime_variable(variable, "0");
}
}

fn set_uv_only_bootstrap() {
let variable = "PYAPP_UV_ONLY_BOOTSTRAP";
if is_enabled(variable) {
set_runtime_variable(variable, "1");
} else {
set_runtime_variable(variable, "0");
}
}

fn set_uv_version() {
let variable = "PYAPP_UV_VERSION";
let version = env::var(variable).unwrap_or("any".to_string());
set_runtime_variable(variable, version);

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())
};
set_runtime_variable("PYAPP__UV_ARTIFACT_NAME", artifact_name);
}

fn set_skip_install() {
let variable = "PYAPP_SKIP_INSTALL";
if is_enabled(variable) {
Expand Down Expand Up @@ -942,6 +982,9 @@ fn main() {
set_pip_project_features();
set_pip_extra_args();
set_pip_allow_config();
set_uv_enabled();
set_uv_only_bootstrap();
set_uv_version();
set_skip_install();
set_indicator();
set_self_command();
Expand Down
14 changes: 14 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ You may set the `PYAPP_DISTRIBUTION_EMBED` option to `true` or `1` to embed the

You can set the `PYAPP_DISTRIBUTION_PATH` option to use a local path rather than fetching the source, which implicitly enables embedding. The local archive should be similar to the [default distributions](#python-distribution) in that there should be a Python interpreter ready for use.

## UV

You may set the `PYAPP_UV_ENABLED` option to `true` or `1` to use [UV](https://github.com/astral-sh/uv) for virtual environment creation and project installation.

### Version ### {: #uv-version }

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.

### 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.

## pip

These options have no effect when the project installation is [disabled](#skipping-project-installation).
Expand Down
18 changes: 15 additions & 3 deletions docs/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ flowchart TD
DISTEMBEDDED -- Yes --> DISTEXTRACT[[Cache from embedded data]]
DISTSOURCE --> FULLISOLATION
DISTEXTRACT --> FULLISOLATION
FULLISOLATION -- No --> VENV[[Create virtual environment]]
FULLISOLATION -- No --> UVENABLED([UV enabled])
UVENABLED -- No --> VENV[[Create virtual environment]]
UVENABLED -- Yes --> UVCACHED([UV cached])
UVCACHED -- No --> DOWNLOADUV[[Download UV]]
UVCACHED -- Yes --> VENV
DOWNLOADUV --> VENV
FULLISOLATION -- Yes --> UNPACK[[Unpack distribution directly]]
UNPACK --> UVENABLEDUNPACK([UV enabled])
UVENABLEDUNPACK -- No --> EXTERNALPIP[[External pip]]
UVENABLEDUNPACK -- Yes --> UVCACHEDUNPACK([UV cached])
UVCACHEDUNPACK -- No --> DOWNLOADUVUNPACK[[Download UV]]
EXTERNALPIP([External pip]) -- No --> PROJEMBEDDED([Project embedded])
EXTERNALPIP -- Yes --> PIPCACHED([pip cached])
PIPCACHED -- No --> DOWNLOADPIP[[Download pip]]
Expand All @@ -32,17 +41,20 @@ flowchart TD
PROJEMBEDDED -- Yes --> PROJEMBED[[Install from embedded data]]
DEPFILE -- No --> SINGLEPROJECT[[Install single project]]
DEPFILE -- Yes --> DEPFILEINSTALL[[Install from dependency file]]
UVCACHEDUNPACK -- Yes --> PROJEMBEDDED
DOWNLOADUVUNPACK --> PROJEMBEDDED
VENV --> EXTERNALPIP
SINGLEPROJECT --> MNG
DEPFILEINSTALL --> MNG
PROJEMBED --> MNG
VENV --> EXTERNALPIP
UNPACK --> EXTERNALPIP
MNG -- No --> EXECUTE[[Execute project]]
MNG -- Yes --> MNGCMD([Command invoked])
MNGCMD -- No --> EXECUTE
MNGCMD -- Yes --> MANAGE[[Run management command]]
click DISTEMBEDDED href "../config/#distribution-embedding"
click FULLISOLATION href "../config/#full-isolation"
click UVENABLED href "../config/#uv"
click UVENABLEDUNPACK href "../config/#uv"
click EXTERNALPIP href "../config/#externally-managed"
click PROJEMBEDDED href "../config/#project-embedding"
click DEPFILE href "../config/#dependency-file"
Expand Down
34 changes: 34 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,26 @@ pub fn pip_external() -> bool {
env!("PYAPP_PIP_EXTERNAL") == "1"
}

pub fn uv_enabled() -> bool {
env!("PYAPP_UV_ENABLED") == "1"
}

pub fn uv_only_bootstrap() -> bool {
env!("PYAPP_UV_ONLY_BOOTSTRAP") == "1"
}

pub fn uv_version() -> String {
env!("PYAPP_UV_VERSION").into()
}

pub fn uv_artifact_name() -> String {
env!("PYAPP__UV_ARTIFACT_NAME").into()
}

pub fn uv_as_installer() -> bool {
uv_enabled() && !uv_only_bootstrap()
}

pub fn is_gui() -> bool {
env!("PYAPP_IS_GUI") == "1"
}
Expand Down Expand Up @@ -235,6 +255,10 @@ pub fn external_pip_cache() -> PathBuf {
cache_dir().join("pip")
}

pub fn managed_uv_cache() -> PathBuf {
cache_dir().join("uv").join(uv_version())
}

pub fn external_pip_zipapp() -> PathBuf {
let pip_version = pip_version();
let filename = if pip_version == "latest" {
Expand All @@ -244,3 +268,13 @@ pub fn external_pip_zipapp() -> PathBuf {
};
external_pip_cache().join(filename)
}

pub fn managed_uv() -> PathBuf {
let uv_artifact_name = uv_artifact_name();
let filename = if uv_artifact_name.ends_with(".zip") {
"uv.exe".to_string()
} else {
"uv".to_string()
};
managed_uv_cache().join(filename)
}
25 changes: 13 additions & 12 deletions src/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,58 @@ use anyhow::{bail, Result};
use crate::terminal;

pub fn unpack(format: String, archive: &PathBuf, destination: &PathBuf) -> Result<()> {
let wait_message = format!("Unpacking distribution ({})", format);
match format.as_ref() {
"tar|bzip2" => unpack_tar_bzip2(archive, destination)?,
"tar|gzip" => unpack_tar_gzip(archive, destination)?,
"tar|zstd" => unpack_tar_zstd(archive, destination)?,
"zip" => unpack_zip(archive, destination)?,
"tar|bzip2" => unpack_tar_bzip2(archive, destination, wait_message)?,
"tar|gzip" => unpack_tar_gzip(archive, destination, wait_message)?,
"tar|zstd" => unpack_tar_zstd(archive, destination, wait_message)?,
"zip" => unpack_zip(archive, destination, wait_message)?,
_ => bail!("unsupported distribution format: {}", format),
}

Ok(())
}

fn unpack_tar_bzip2(path: &PathBuf, destination: &PathBuf) -> Result<()> {
fn unpack_tar_bzip2(path: &PathBuf, destination: &PathBuf, wait_message: String) -> Result<()> {
let bz = bzip2::read::BzDecoder::new(File::open(path)?);
let mut archive = tar::Archive::new(bz);

let spinner = terminal::spinner("Unpacking distribution (tar|bzip2)".to_string());
let spinner = terminal::spinner(wait_message);
let result = archive.unpack(destination);
spinner.finish_and_clear();
result?;

Ok(())
}

fn unpack_tar_gzip(path: &PathBuf, destination: &PathBuf) -> Result<()> {
pub fn unpack_tar_gzip(path: &PathBuf, destination: &PathBuf, wait_message: String) -> Result<()> {
let gz = flate2::read::GzDecoder::new(File::open(path)?);
let mut archive = tar::Archive::new(gz);

let spinner = terminal::spinner("Unpacking distribution (tar|gzip)".to_string());
let spinner = terminal::spinner(wait_message);
let result = archive.unpack(destination);
spinner.finish_and_clear();
result?;

Ok(())
}

fn unpack_tar_zstd(path: &PathBuf, destination: &PathBuf) -> Result<()> {
fn unpack_tar_zstd(path: &PathBuf, destination: &PathBuf, wait_message: String) -> Result<()> {
let zst = zstd::stream::read::Decoder::new(File::open(path)?)?;
let mut archive = tar::Archive::new(zst);

let spinner = terminal::spinner("Unpacking distribution (tar|zstd)".to_string());
let spinner = terminal::spinner(wait_message);
let result = archive.unpack(destination);
spinner.finish_and_clear();
result?;

Ok(())
}

fn unpack_zip(path: &PathBuf, destination: &PathBuf) -> Result<()> {
pub fn unpack_zip(path: &PathBuf, destination: &PathBuf, wait_message: String) -> Result<()> {
let mut archive = zip::ZipArchive::new(File::open(path)?)?;

let spinner = terminal::spinner("Unpacking distribution (zip)".to_string());
let spinner = terminal::spinner(wait_message);
let result = archive.extract(destination);
spinner.finish_and_clear();
result?;
Expand Down
Loading

0 comments on commit 6a256fa

Please sign in to comment.