From c4c5378c0bb61ecf4e73afb8919509ed9ec445ec Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 24 Sep 2024 19:23:17 +0200 Subject: [PATCH] Add build backend scaffolding (#7662) --- crates/uv-cli/src/lib.rs | 39 +++++++ crates/uv/src/commands/build_backend.rs | 41 +++++++ crates/uv/src/commands/mod.rs | 8 +- crates/uv/src/lib.rs | 38 +++++- python/uv/__init__.py | 63 +++++----- python/uv/_build_backend.py | 110 ++++++++++++++++++ python/uv/_find_uv.py | 36 ++++++ scripts/packages/uv_backend/.gitignore | 1 + scripts/packages/uv_backend/README.md | 3 + scripts/packages/uv_backend/pyproject.toml | 11 ++ .../uv_backend/src/uv_backend/__init__.py | 2 + 11 files changed, 309 insertions(+), 43 deletions(-) create mode 100644 crates/uv/src/commands/build_backend.rs create mode 100644 python/uv/_build_backend.py create mode 100644 python/uv/_find_uv.py create mode 100644 scripts/packages/uv_backend/.gitignore create mode 100644 scripts/packages/uv_backend/README.md create mode 100644 scripts/packages/uv_backend/pyproject.toml create mode 100644 scripts/packages/uv_backend/src/uv_backend/__init__.py diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index affebc37f30e..9d143d521480 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -389,6 +389,15 @@ pub enum Commands { Build(BuildArgs), /// Upload distributions to an index. Publish(PublishArgs), + /// The implementation of the build backend. + /// + /// These commands are not directly exposed to the user, instead users invoke their build + /// frontend (PEP 517) which calls the Python shims which calls back into uv with this method. + #[command(hide = true)] + BuildBackend { + #[command(subcommand)] + command: BuildBackendCommand, + }, /// Manage uv's cache. #[command( after_help = "Use `uv help cache` for more details.", @@ -4389,3 +4398,33 @@ pub struct PublishArgs { )] pub allow_insecure_host: Option>>, } + +/// See [PEP 517](https://peps.python.org/pep-0517/) and +/// [PEP 660](https://peps.python.org/pep-0660/) for specifications of the parameters. +#[derive(Subcommand)] +pub enum BuildBackendCommand { + /// PEP 517 hook `build_sdist`. + BuildSdist { sdist_directory: PathBuf }, + /// PEP 517 hook `build_wheel`. + BuildWheel { + wheel_directory: PathBuf, + #[arg(long)] + metadata_directory: Option, + }, + /// PEP 660 hook `build_editable`. + BuildEditable { + wheel_directory: PathBuf, + #[arg(long)] + metadata_directory: Option, + }, + /// PEP 517 hook `get_requires_for_build_sdist`. + GetRequiresForBuildSdist, + /// PEP 517 hook `get_requires_for_build_wheel`. + GetRequiresForBuildWheel, + /// PEP 517 hook `prepare_metadata_for_build_wheel`. + PrepareMetadataForBuildWheel { wheel_directory: PathBuf }, + /// PEP 660 hook `get_requires_for_build_editable`. + GetRequiresForBuildEditable, + /// PEP 660 hook `prepare_metadata_for_build_editable`. + PrepareMetadataForBuildEditable { wheel_directory: PathBuf }, +} diff --git a/crates/uv/src/commands/build_backend.rs b/crates/uv/src/commands/build_backend.rs new file mode 100644 index 000000000000..57ecd0c25d09 --- /dev/null +++ b/crates/uv/src/commands/build_backend.rs @@ -0,0 +1,41 @@ +use crate::commands::ExitStatus; +use anyhow::Result; +use std::path::Path; + +pub(crate) fn build_sdist(_sdist_directory: &Path) -> Result { + todo!() +} + +pub(crate) fn build_wheel( + _wheel_directory: &Path, + _metadata_directory: Option<&Path>, +) -> Result { + todo!() +} + +pub(crate) fn build_editable( + _wheel_directory: &Path, + _metadata_directory: Option<&Path>, +) -> Result { + todo!() +} + +pub(crate) fn get_requires_for_build_sdist() -> Result { + todo!() +} + +pub(crate) fn get_requires_for_build_wheel() -> Result { + todo!() +} + +pub(crate) fn prepare_metadata_for_build_wheel(_wheel_directory: &Path) -> Result { + todo!() +} + +pub(crate) fn get_requires_for_build_editable() -> Result { + todo!() +} + +pub(crate) fn prepare_metadata_for_build_editable(_wheel_directory: &Path) -> Result { + todo!() +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 48d72ab2de35..23723e93f480 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -60,20 +60,20 @@ pub(crate) use version::version; use crate::printer::Printer; +mod build; +pub(crate) mod build_backend; mod cache_clean; mod cache_dir; mod cache_prune; mod help; pub(crate) mod pip; mod project; +mod publish; mod python; pub(crate) mod reporters; -mod tool; - -mod build; -mod publish; #[cfg(feature = "self-update")] mod self_update; +mod tool; mod venv; mod version; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e81b5b09089d..ada7dc88532d 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -15,8 +15,8 @@ use tracing::{debug, instrument}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::{ - compat::CompatArgs, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, - ProjectCommand, + compat::CompatArgs, BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, + PipCommand, PipNamespace, ProjectCommand, }; use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs}; #[cfg(feature = "self-update")] @@ -1118,6 +1118,40 @@ async fn run(cli: Cli) -> Result { ) .await } + Commands::BuildBackend { command } => match command { + BuildBackendCommand::BuildSdist { sdist_directory } => { + commands::build_backend::build_sdist(&sdist_directory) + } + BuildBackendCommand::BuildWheel { + wheel_directory, + metadata_directory, + } => commands::build_backend::build_wheel( + &wheel_directory, + metadata_directory.as_deref(), + ), + BuildBackendCommand::BuildEditable { + wheel_directory, + metadata_directory, + } => commands::build_backend::build_editable( + &wheel_directory, + metadata_directory.as_deref(), + ), + BuildBackendCommand::GetRequiresForBuildSdist => { + commands::build_backend::get_requires_for_build_sdist() + } + BuildBackendCommand::GetRequiresForBuildWheel => { + commands::build_backend::get_requires_for_build_wheel() + } + BuildBackendCommand::PrepareMetadataForBuildWheel { wheel_directory } => { + commands::build_backend::prepare_metadata_for_build_wheel(&wheel_directory) + } + BuildBackendCommand::GetRequiresForBuildEditable => { + commands::build_backend::get_requires_for_build_editable() + } + BuildBackendCommand::PrepareMetadataForBuildEditable { wheel_directory } => { + commands::build_backend::prepare_metadata_for_build_editable(&wheel_directory) + } + }, } } diff --git a/python/uv/__init__.py b/python/uv/__init__.py index 781eee4f97bf..5ad0fde7c6d9 100644 --- a/python/uv/__init__.py +++ b/python/uv/__init__.py @@ -1,41 +1,30 @@ from __future__ import annotations import os -import sys -import sysconfig - -def find_uv_bin() -> str: - """Return the uv binary path.""" - - uv_exe = "uv" + sysconfig.get_config_var("EXE") - - path = os.path.join(sysconfig.get_path("scripts"), uv_exe) - if os.path.isfile(path): - return path - - if sys.version_info >= (3, 10): - user_scheme = sysconfig.get_preferred_scheme("user") - elif os.name == "nt": - user_scheme = "nt_user" - elif sys.platform == "darwin" and sys._framework: - user_scheme = "osx_framework_user" - else: - user_scheme = "posix_user" - - path = os.path.join(sysconfig.get_path("scripts", scheme=user_scheme), uv_exe) - if os.path.isfile(path): - return path - - # Search in `bin` adjacent to package root (as created by `pip install --target`). - pkg_root = os.path.dirname(os.path.dirname(__file__)) - target_path = os.path.join(pkg_root, "bin", uv_exe) - if os.path.isfile(target_path): - return target_path - - raise FileNotFoundError(path) - - -__all__ = [ - "find_uv_bin", -] +if os.environ.get("UV_PREVIEW"): + from ._build_backend import * +from ._find_uv import find_uv_bin + +if os.environ.get("UV_PREVIEW"): + __all__ = [ + "find_uv_bin", + # PEP 517 hook `build_sdist`. + "build_sdist", + # PEP 517 hook `build_wheel`. + "build_wheel", + # PEP 660 hook `build_editable`. + "build_editable", + # PEP 517 hook `get_requires_for_build_sdist`. + "get_requires_for_build_sdist", + # PEP 517 hook `get_requires_for_build_wheel`. + "get_requires_for_build_wheel", + # PEP 517 hook `prepare_metadata_for_build_wheel`. + "prepare_metadata_for_build_wheel", + # PEP 660 hook `get_requires_for_build_editable`. + "get_requires_for_build_editable", + # PEP 660 hook `prepare_metadata_for_build_editable`. + "prepare_metadata_for_build_editable", + ] +else: + __all__ = ["find_uv_bin"] diff --git a/python/uv/_build_backend.py b/python/uv/_build_backend.py new file mode 100644 index 000000000000..a1284fe40b73 --- /dev/null +++ b/python/uv/_build_backend.py @@ -0,0 +1,110 @@ +""" +Python shims for the PEP 517 and PEP 660 build backend. + +Major imports in this module are required to be lazy: +``` +$ hyperfine \ + "/usr/bin/python3 -c \"print('hi')\"" \ + "/usr/bin/python3 -c \"from subprocess import check_call; print('hi')\"" +Base: Time (mean ± σ): 11.0 ms ± 1.7 ms [User: 8.5 ms, System: 2.5 ms] +With import: Time (mean ± σ): 15.2 ms ± 2.0 ms [User: 12.3 ms, System: 2.9 ms] +Base 1.38 ± 0.28 times faster than with import +``` + +The same thing goes for the typing module, so we use Python 3.10 type annotations that +don't require importing typing but then quote them so earlier Python version ignore +them while IDEs and type checker can see through the quotes. +""" + + +def warn_config_settings(config_settings: "dict | None" = None): + import sys + + if config_settings: + print("Warning: Config settings are not supported", file=sys.stderr) + + +def call(args: "list[str]", config_settings: "dict | None" = None) -> str: + """Invoke a uv subprocess and return the filename from stdout.""" + import subprocess + import sys + + from ._find_uv import find_uv_bin + + warn_config_settings(config_settings) + # Forward stderr, capture stdout for the filename + result = subprocess.run([find_uv_bin()] + args, stdout=subprocess.PIPE) + if result.returncode != 0: + sys.exit(result.returncode) + # If there was extra stdout, forward it (there should not be extra stdout) + result = result.stdout.decode("utf-8").strip().splitlines(keepends=True) + sys.stdout.writelines(result[:-1]) + # Fail explicitly instead of an irrelevant stacktrace + if not result: + print("uv subprocess did not return a filename on stdout", file=sys.stderr) + sys.exit(1) + return result[-1].strip() + + +def build_sdist(sdist_directory: str, config_settings: "dict | None" = None): + """PEP 517 hook `build_sdist`.""" + args = ["build-backend", "build-sdist", sdist_directory] + return call(args, config_settings) + + +def build_wheel( + wheel_directory: str, + config_settings: "dict | None" = None, + metadata_directory: "str | None" = None, +): + """PEP 517 hook `build_wheel`.""" + args = ["build-backend", "build-wheel", wheel_directory] + if metadata_directory: + args.extend(["--metadata-directory", metadata_directory]) + return call(args, config_settings) + + +def get_requires_for_build_sdist(config_settings: "dict | None" = None): + """PEP 517 hook `get_requires_for_build_sdist`.""" + warn_config_settings(config_settings) + return [] + + +def get_requires_for_build_wheel(config_settings: "dict | None" = None): + """PEP 517 hook `get_requires_for_build_wheel`.""" + warn_config_settings(config_settings) + return [] + + +def prepare_metadata_for_build_wheel( + metadata_directory: str, config_settings: "dict | None" = None +): + """PEP 517 hook `prepare_metadata_for_build_wheel`.""" + args = ["build-backend", "prepare-metadata-for-build-wheel", metadata_directory] + return call(args, config_settings) + + +def build_editable( + wheel_directory: str, + config_settings: "dict | None" = None, + metadata_directory: "str | None" = None, +): + """PEP 660 hook `build_editable`.""" + args = ["build-backend", "build-editable", wheel_directory] + if metadata_directory: + args.extend(["--metadata-directory", metadata_directory]) + return call(args, config_settings) + + +def get_requires_for_build_editable(config_settings: "dict | None" = None): + """PEP 660 hook `get_requires_for_build_editable`.""" + warn_config_settings(config_settings) + return [] + + +def prepare_metadata_for_build_editable( + metadata_directory: str, config_settings: "dict | None" = None +): + """PEP 660 hook `prepare_metadata_for_build_editable`.""" + args = ["build-backend", "prepare-metadata-for-build-editable", metadata_directory] + return call(args, config_settings) diff --git a/python/uv/_find_uv.py b/python/uv/_find_uv.py new file mode 100644 index 000000000000..00b7d887370f --- /dev/null +++ b/python/uv/_find_uv.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import os +import sys +import sysconfig + + +def find_uv_bin() -> str: + """Return the uv binary path.""" + + uv_exe = "uv" + sysconfig.get_config_var("EXE") + + path = os.path.join(sysconfig.get_path("scripts"), uv_exe) + if os.path.isfile(path): + return path + + if sys.version_info >= (3, 10): + user_scheme = sysconfig.get_preferred_scheme("user") + elif os.name == "nt": + user_scheme = "nt_user" + elif sys.platform == "darwin" and sys._framework: + user_scheme = "osx_framework_user" + else: + user_scheme = "posix_user" + + path = os.path.join(sysconfig.get_path("scripts", scheme=user_scheme), uv_exe) + if os.path.isfile(path): + return path + + # Search in `bin` adjacent to package root (as created by `pip install --target`). + pkg_root = os.path.dirname(os.path.dirname(__file__)) + target_path = os.path.join(pkg_root, "bin", uv_exe) + if os.path.isfile(target_path): + return target_path + + raise FileNotFoundError(path) diff --git a/scripts/packages/uv_backend/.gitignore b/scripts/packages/uv_backend/.gitignore new file mode 100644 index 000000000000..849ddff3b7ec --- /dev/null +++ b/scripts/packages/uv_backend/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/scripts/packages/uv_backend/README.md b/scripts/packages/uv_backend/README.md new file mode 100644 index 000000000000..4c3573792a82 --- /dev/null +++ b/scripts/packages/uv_backend/README.md @@ -0,0 +1,3 @@ +# uv_backend + +A simple package to be built with the uv build backend. diff --git a/scripts/packages/uv_backend/pyproject.toml b/scripts/packages/uv_backend/pyproject.toml new file mode 100644 index 000000000000..829a376e2100 --- /dev/null +++ b/scripts/packages/uv_backend/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "uv-backend" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[build-system] +requires = ["uv>=0.4.15,<5"] +build-backend = "uv" diff --git a/scripts/packages/uv_backend/src/uv_backend/__init__.py b/scripts/packages/uv_backend/src/uv_backend/__init__.py new file mode 100644 index 000000000000..8b0a54495db3 --- /dev/null +++ b/scripts/packages/uv_backend/src/uv_backend/__init__.py @@ -0,0 +1,2 @@ +def greet(): + print("Hello 👋")