mirror of https://github.com/astral-sh/uv
Add support for `--no-build-isolation` (#2258)
## Summary This PR adds support for pip's `--no-build-isolation`. When enabled, build requirements won't be installed during PEP 517-style builds, but the source environment _will_ be used when executing the build steps themselves. Closes https://github.com/astral-sh/uv/issues/1715.
This commit is contained in:
parent
d249574a47
commit
5ae5980c88
|
|
@ -307,7 +307,6 @@ the implementation, and tend to be tracked in individual issues. For example:
|
||||||
|
|
||||||
- [`--trusted-host`](https://github.com/astral-sh/uv/issues/1339)
|
- [`--trusted-host`](https://github.com/astral-sh/uv/issues/1339)
|
||||||
- [`--user`](https://github.com/astral-sh/uv/issues/2077)
|
- [`--user`](https://github.com/astral-sh/uv/issues/2077)
|
||||||
- [`--no-build-isolation`](https://github.com/astral-sh/uv/issues/1715)
|
|
||||||
|
|
||||||
If you encounter a missing option or subcommand, please search the issue tracker to see if it has
|
If you encounter a missing option or subcommand, please search the issue tracker to see if it has
|
||||||
already been reported, and if not, consider opening a new issue. Feel free to upvote any existing
|
already been reported, and if not, consider opening a new issue. Feel free to upvote any existing
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ use distribution_types::Resolution;
|
||||||
use pep508_rs::Requirement;
|
use pep508_rs::Requirement;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
use uv_traits::{BuildContext, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait};
|
use uv_traits::{
|
||||||
|
BuildContext, BuildIsolation, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait,
|
||||||
|
};
|
||||||
|
|
||||||
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
|
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
|
||||||
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
|
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
|
@ -357,6 +359,7 @@ impl SourceBuild {
|
||||||
package_id: String,
|
package_id: String,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: ConfigSettings,
|
config_settings: ConfigSettings,
|
||||||
|
build_isolation: BuildIsolation<'_>,
|
||||||
build_kind: BuildKind,
|
build_kind: BuildKind,
|
||||||
mut environment_variables: FxHashMap<OsString, OsString>,
|
mut environment_variables: FxHashMap<OsString, OsString>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
|
|
@ -402,15 +405,21 @@ impl SourceBuild {
|
||||||
let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend)
|
let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend)
|
||||||
.map_err(|err| *err)?;
|
.map_err(|err| *err)?;
|
||||||
|
|
||||||
let venv = uv_virtualenv::create_venv(
|
// Create a virtual environment, or install into the shared environment if requested.
|
||||||
|
let venv = match build_isolation {
|
||||||
|
BuildIsolation::Isolated => uv_virtualenv::create_venv(
|
||||||
&temp_dir.path().join(".venv"),
|
&temp_dir.path().join(".venv"),
|
||||||
interpreter.clone(),
|
interpreter.clone(),
|
||||||
uv_virtualenv::Prompt::None,
|
uv_virtualenv::Prompt::None,
|
||||||
false,
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)?;
|
)?,
|
||||||
|
BuildIsolation::Shared(venv) => venv.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
// Setup the build environment.
|
// Setup the build environment. If build isolation is disabled, we assume the build
|
||||||
|
// environment is already setup.
|
||||||
|
if build_isolation.is_isolated() {
|
||||||
let resolved_requirements = Self::get_resolved_requirements(
|
let resolved_requirements = Self::get_resolved_requirements(
|
||||||
build_context,
|
build_context,
|
||||||
source_build_context,
|
source_build_context,
|
||||||
|
|
@ -422,7 +431,10 @@ impl SourceBuild {
|
||||||
build_context
|
build_context
|
||||||
.install(&resolved_requirements, &venv)
|
.install(&resolved_requirements, &venv)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| Error::RequirementsInstall("build-system.requires (install)", err))?;
|
.map_err(|err| {
|
||||||
|
Error::RequirementsInstall("build-system.requires (install)", err)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
// Figure out what the modified path should be
|
// Figure out what the modified path should be
|
||||||
// Remove the PATH variable from the environment variables if it's there
|
// Remove the PATH variable from the environment variables if it's there
|
||||||
|
|
@ -454,6 +466,9 @@ impl SourceBuild {
|
||||||
OsString::from(venv.scripts())
|
OsString::from(venv.scripts())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create the PEP 517 build environment. If build isolation is disabled, we assume the build
|
||||||
|
// environment is already setup.
|
||||||
|
if build_isolation.is_isolated() {
|
||||||
if let Some(pep517_backend) = &pep517_backend {
|
if let Some(pep517_backend) = &pep517_backend {
|
||||||
create_pep517_build_environment(
|
create_pep517_build_environment(
|
||||||
&source_tree,
|
&source_tree,
|
||||||
|
|
@ -468,6 +483,7 @@ impl SourceBuild {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
temp_dir,
|
temp_dir,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ use uv_dispatch::BuildDispatch;
|
||||||
use uv_installer::NoBinary;
|
use uv_installer::NoBinary;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_resolver::InMemoryIndex;
|
use uv_resolver::InMemoryIndex;
|
||||||
use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{
|
||||||
|
BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub(crate) struct BuildArgs {
|
pub(crate) struct BuildArgs {
|
||||||
|
|
@ -74,6 +76,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
BuildIsolation::Isolated,
|
||||||
&NoBuild::None,
|
&NoBuild::None,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
);
|
);
|
||||||
|
|
@ -87,6 +90,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
|
||||||
args.sdist.display().to_string(),
|
args.sdist.display().to_string(),
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings.clone(),
|
config_settings.clone(),
|
||||||
|
BuildIsolation::Isolated,
|
||||||
build_kind,
|
build_kind,
|
||||||
FxHashMap::default(),
|
FxHashMap::default(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ use uv_installer::{Downloader, NoBinary};
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_resolver::{DistFinder, InMemoryIndex};
|
use uv_resolver::{DistFinder, InMemoryIndex};
|
||||||
use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub(crate) struct InstallManyArgs {
|
pub(crate) struct InstallManyArgs {
|
||||||
|
|
@ -81,6 +81,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
BuildIsolation::Isolated,
|
||||||
&no_build,
|
&no_build,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch;
|
||||||
use uv_installer::NoBinary;
|
use uv_installer::NoBinary;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
|
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
|
||||||
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
#[derive(ValueEnum, Default, Clone)]
|
#[derive(ValueEnum, Default, Clone)]
|
||||||
pub(crate) enum ResolveCliFormat {
|
pub(crate) enum ResolveCliFormat {
|
||||||
|
|
@ -85,6 +85,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
|
||||||
&in_flight,
|
&in_flight,
|
||||||
SetupPyStrategy::default(),
|
SetupPyStrategy::default(),
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
BuildIsolation::Isolated,
|
||||||
&no_build,
|
&no_build,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use uv_installer::NoBinary;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_resolver::InMemoryIndex;
|
use uv_resolver::InMemoryIndex;
|
||||||
use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub(crate) struct ResolveManyArgs {
|
pub(crate) struct ResolveManyArgs {
|
||||||
|
|
@ -109,6 +109,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
BuildIsolation::Isolated,
|
||||||
&no_build,
|
&no_build,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ use uv_client::{FlatIndex, RegistryClient};
|
||||||
use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages};
|
use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
|
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
|
||||||
use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{
|
||||||
|
BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
|
/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
|
||||||
/// documentation.
|
/// documentation.
|
||||||
|
|
@ -33,6 +35,7 @@ pub struct BuildDispatch<'a> {
|
||||||
index: &'a InMemoryIndex,
|
index: &'a InMemoryIndex,
|
||||||
in_flight: &'a InFlight,
|
in_flight: &'a InFlight,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
|
build_isolation: BuildIsolation<'a>,
|
||||||
no_build: &'a NoBuild,
|
no_build: &'a NoBuild,
|
||||||
no_binary: &'a NoBinary,
|
no_binary: &'a NoBinary,
|
||||||
config_settings: &'a ConfigSettings,
|
config_settings: &'a ConfigSettings,
|
||||||
|
|
@ -53,6 +56,7 @@ impl<'a> BuildDispatch<'a> {
|
||||||
in_flight: &'a InFlight,
|
in_flight: &'a InFlight,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: &'a ConfigSettings,
|
config_settings: &'a ConfigSettings,
|
||||||
|
build_isolation: BuildIsolation<'a>,
|
||||||
no_build: &'a NoBuild,
|
no_build: &'a NoBuild,
|
||||||
no_binary: &'a NoBinary,
|
no_binary: &'a NoBinary,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -66,6 +70,7 @@ impl<'a> BuildDispatch<'a> {
|
||||||
in_flight,
|
in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
build_isolation,
|
||||||
no_build,
|
no_build,
|
||||||
no_binary,
|
no_binary,
|
||||||
source_build_context: SourceBuildContext::default(),
|
source_build_context: SourceBuildContext::default(),
|
||||||
|
|
@ -107,6 +112,10 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
self.interpreter
|
self.interpreter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_isolation(&self) -> BuildIsolation {
|
||||||
|
self.build_isolation
|
||||||
|
}
|
||||||
|
|
||||||
fn no_build(&self) -> &NoBuild {
|
fn no_build(&self) -> &NoBuild {
|
||||||
self.no_build
|
self.no_build
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +189,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
local,
|
local,
|
||||||
remote,
|
remote,
|
||||||
reinstalls,
|
reinstalls,
|
||||||
extraneous,
|
extraneous: _,
|
||||||
} = Planner::with_requirements(&resolution.requirements()).build(
|
} = Planner::with_requirements(&resolution.requirements()).build(
|
||||||
site_packages,
|
site_packages,
|
||||||
&Reinstall::None,
|
&Reinstall::None,
|
||||||
|
|
@ -191,6 +200,12 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
tags,
|
tags,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Nothing to do.
|
||||||
|
if remote.is_empty() && local.is_empty() && reinstalls.is_empty() {
|
||||||
|
debug!("No build requirements to install for build");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve any registry-based requirements.
|
// Resolve any registry-based requirements.
|
||||||
let remote = remote
|
let remote = remote
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -207,7 +222,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
vec![]
|
vec![]
|
||||||
} else {
|
} else {
|
||||||
// TODO(konstin): Check that there is no endless recursion.
|
// TODO(konstin): Check that there is no endless recursion.
|
||||||
let downloader = Downloader::new(self.cache(), tags, self.client, self);
|
let downloader = Downloader::new(self.cache, tags, self.client, self);
|
||||||
debug!(
|
debug!(
|
||||||
"Downloading and building requirement{} for build: {}",
|
"Downloading and building requirement{} for build: {}",
|
||||||
if remote.len() == 1 { "" } else { "s" },
|
if remote.len() == 1 { "" } else { "s" },
|
||||||
|
|
@ -221,8 +236,8 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any unnecessary packages.
|
// Remove any unnecessary packages.
|
||||||
if !extraneous.is_empty() || !reinstalls.is_empty() {
|
if !reinstalls.is_empty() {
|
||||||
for dist_info in extraneous.iter().chain(reinstalls.iter()) {
|
for dist_info in &reinstalls {
|
||||||
let summary = uv_installer::uninstall(dist_info)
|
let summary = uv_installer::uninstall(dist_info)
|
||||||
.await
|
.await
|
||||||
.context("Failed to uninstall build dependencies")?;
|
.context("Failed to uninstall build dependencies")?;
|
||||||
|
|
@ -295,6 +310,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
package_id.to_string(),
|
package_id.to_string(),
|
||||||
self.setup_py,
|
self.setup_py,
|
||||||
self.config_settings.clone(),
|
self.config_settings.clone(),
|
||||||
|
self.build_isolation,
|
||||||
build_kind,
|
build_kind,
|
||||||
self.build_extra_env_vars.clone(),
|
self.build_extra_env_vars.clone(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -949,6 +949,8 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
|
||||||
editable_wheel_dir: &Path,
|
editable_wheel_dir: &Path,
|
||||||
) -> Result<(Dist, String, WheelFilename, Metadata21), Error> {
|
) -> Result<(Dist, String, WheelFilename, Metadata21), Error> {
|
||||||
debug!("Building (editable) {editable}");
|
debug!("Building (editable) {editable}");
|
||||||
|
|
||||||
|
// Build the wheel.
|
||||||
let disk_filename = self
|
let disk_filename = self
|
||||||
.build_context
|
.build_context
|
||||||
.setup_build(
|
.setup_build(
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,11 @@ impl PythonEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory.
|
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory.
|
||||||
pub fn from_interpreter(interpreter: Interpreter, root: PathBuf) -> Self {
|
pub fn from_interpreter(interpreter: Interpreter) -> Self {
|
||||||
Self { root, interpreter }
|
Self {
|
||||||
|
root: interpreter.prefix().to_path_buf(),
|
||||||
|
interpreter,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the location of the Python interpreter.
|
/// Returns the location of the Python interpreter.
|
||||||
|
|
@ -100,7 +103,7 @@ impl PythonEnvironment {
|
||||||
self.interpreter.scripts()
|
self.interpreter.scripts()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lock the virtual environment to prevent concurrent writes.
|
/// Grab a file lock for the virtual environment to prevent concurrent writes across processes.
|
||||||
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
||||||
if self.interpreter.is_virtualenv() {
|
if self.interpreter.is_virtualenv() {
|
||||||
// If the environment a virtualenv, use a virtualenv-specific lock file.
|
// If the environment a virtualenv, use a virtualenv-specific lock file.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ use uv_resolver::{
|
||||||
DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
||||||
ResolutionGraph, ResolutionMode, Resolver,
|
ResolutionGraph, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_traits::{BuildContext, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait};
|
use uv_traits::{
|
||||||
|
BuildContext, BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait,
|
||||||
|
};
|
||||||
|
|
||||||
// Exclude any packages uploaded after this date.
|
// Exclude any packages uploaded after this date.
|
||||||
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
|
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
|
||||||
|
|
@ -57,6 +59,10 @@ impl BuildContext for DummyContext {
|
||||||
&self.interpreter
|
&self.interpreter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_isolation(&self) -> BuildIsolation {
|
||||||
|
BuildIsolation::Isolated
|
||||||
|
}
|
||||||
|
|
||||||
fn no_build(&self) -> &NoBuild {
|
fn no_build(&self) -> &NoBuild {
|
||||||
&NoBuild::None
|
&NoBuild::None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,16 +54,19 @@ use uv_normalize::PackageName;
|
||||||
/// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between
|
/// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between
|
||||||
/// them.
|
/// them.
|
||||||
|
|
||||||
// TODO(konstin): Proper error types
|
|
||||||
pub trait BuildContext: Sync {
|
pub trait BuildContext: Sync {
|
||||||
type SourceDistBuilder: SourceBuildTrait + Send + Sync;
|
type SourceDistBuilder: SourceBuildTrait + Send + Sync;
|
||||||
|
|
||||||
|
/// Return a reference to the cache.
|
||||||
fn cache(&self) -> &Cache;
|
fn cache(&self) -> &Cache;
|
||||||
|
|
||||||
/// All (potentially nested) source distribution builds use the same base python and can reuse
|
/// All (potentially nested) source distribution builds use the same base python and can reuse
|
||||||
/// it's metadata (e.g. wheel compatibility tags).
|
/// it's metadata (e.g. wheel compatibility tags).
|
||||||
fn interpreter(&self) -> &Interpreter;
|
fn interpreter(&self) -> &Interpreter;
|
||||||
|
|
||||||
|
/// Whether to enforce build isolation when building source distributions.
|
||||||
|
fn build_isolation(&self) -> BuildIsolation;
|
||||||
|
|
||||||
/// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls
|
/// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls
|
||||||
/// will fail in this case. This method exists to avoid fetching source distributions if we know
|
/// will fail in this case. This method exists to avoid fetching source distributions if we know
|
||||||
/// we can't build them
|
/// we can't build them
|
||||||
|
|
@ -137,6 +140,20 @@ pub struct InFlight {
|
||||||
pub downloads: OnceMap<DistributionId, Result<CachedDist, String>>,
|
pub downloads: OnceMap<DistributionId, Result<CachedDist, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to enforce build isolation when building source distributions.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum BuildIsolation<'a> {
|
||||||
|
Isolated,
|
||||||
|
Shared(&'a PythonEnvironment),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BuildIsolation<'a> {
|
||||||
|
/// Returns `true` if build isolation is enforced.
|
||||||
|
pub fn is_isolated(&self) -> bool {
|
||||||
|
matches!(self, Self::Isolated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The strategy to use when building source distributions that lack a `pyproject.toml`.
|
/// The strategy to use when building source distributions that lack a `pyproject.toml`.
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum SetupPyStrategy {
|
pub enum SetupPyStrategy {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,5 @@ pub fn create_venv(
|
||||||
|
|
||||||
// Create the corresponding `PythonEnvironment`.
|
// Create the corresponding `PythonEnvironment`.
|
||||||
let interpreter = interpreter.with_virtualenv(virtualenv);
|
let interpreter = interpreter.with_virtualenv(virtualenv);
|
||||||
let root = interpreter.prefix().to_path_buf();
|
Ok(PythonEnvironment::from_interpreter(interpreter))
|
||||||
Ok(PythonEnvironment::from_interpreter(interpreter, root))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder}
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{Downloader, NoBinary};
|
use uv_installer::{Downloader, NoBinary};
|
||||||
use uv_interpreter::{Interpreter, PythonVersion};
|
use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_resolver::{
|
use uv_resolver::{
|
||||||
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
|
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
|
||||||
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver,
|
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, ResolverReporter};
|
use crate::commands::reporters::{DownloadReporter, ResolverReporter};
|
||||||
|
|
@ -62,6 +62,7 @@ pub(crate) async fn pip_compile(
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: ConfigSettings,
|
config_settings: ConfigSettings,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
no_build_isolation: bool,
|
||||||
no_build: &NoBuild,
|
no_build: &NoBuild,
|
||||||
python_version: Option<PythonVersion>,
|
python_version: Option<PythonVersion>,
|
||||||
exclude_newer: Option<DateTime<Utc>>,
|
exclude_newer: Option<DateTime<Utc>>,
|
||||||
|
|
@ -137,6 +138,7 @@ pub(crate) async fn pip_compile(
|
||||||
interpreter.python_version(),
|
interpreter.python_version(),
|
||||||
interpreter.sys_executable().simplified_display().cyan()
|
interpreter.sys_executable().simplified_display().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(python_version) = python_version.as_ref() {
|
if let Some(python_version) = python_version.as_ref() {
|
||||||
// If the requested version does not match the version we're using warn the user
|
// If the requested version does not match the version we're using warn the user
|
||||||
// _unless_ they have not specified a patch version and that is the only difference
|
// _unless_ they have not specified a patch version and that is the only difference
|
||||||
|
|
@ -203,6 +205,15 @@ pub(crate) async fn pip_compile(
|
||||||
// Track in-flight downloads, builds, etc., across resolutions.
|
// Track in-flight downloads, builds, etc., across resolutions.
|
||||||
let in_flight = InFlight::default();
|
let in_flight = InFlight::default();
|
||||||
|
|
||||||
|
// Determine whether to enable build isolation.
|
||||||
|
let venv;
|
||||||
|
let build_isolation = if no_build_isolation {
|
||||||
|
venv = PythonEnvironment::from_interpreter(interpreter.clone());
|
||||||
|
BuildIsolation::Shared(&venv)
|
||||||
|
} else {
|
||||||
|
BuildIsolation::Isolated
|
||||||
|
};
|
||||||
|
|
||||||
let build_dispatch = BuildDispatch::new(
|
let build_dispatch = BuildDispatch::new(
|
||||||
&client,
|
&client,
|
||||||
&cache,
|
&cache,
|
||||||
|
|
@ -213,6 +224,7 @@ pub(crate) async fn pip_compile(
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
build_isolation,
|
||||||
no_build,
|
no_build,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ use uv_resolver::{
|
||||||
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
||||||
ResolutionGraph, ResolutionMode, Resolver,
|
ResolutionGraph, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||||
|
|
@ -59,6 +59,7 @@ pub(crate) async fn pip_install(
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
no_build_isolation: bool,
|
||||||
no_build: &NoBuild,
|
no_build: &NoBuild,
|
||||||
no_binary: &NoBinary,
|
no_binary: &NoBinary,
|
||||||
strict: bool,
|
strict: bool,
|
||||||
|
|
@ -190,6 +191,13 @@ pub(crate) async fn pip_install(
|
||||||
FlatIndex::from_entries(entries, tags)
|
FlatIndex::from_entries(entries, tags)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine whether to enable build isolation.
|
||||||
|
let build_isolation = if no_build_isolation {
|
||||||
|
BuildIsolation::Shared(&venv)
|
||||||
|
} else {
|
||||||
|
BuildIsolation::Isolated
|
||||||
|
};
|
||||||
|
|
||||||
// Create a shared in-memory index.
|
// Create a shared in-memory index.
|
||||||
let index = InMemoryIndex::default();
|
let index = InMemoryIndex::default();
|
||||||
|
|
||||||
|
|
@ -206,6 +214,7 @@ pub(crate) async fn pip_install(
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
build_isolation,
|
||||||
no_build,
|
no_build,
|
||||||
no_binary,
|
no_binary,
|
||||||
)
|
)
|
||||||
|
|
@ -289,6 +298,7 @@ pub(crate) async fn pip_install(
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
build_isolation,
|
||||||
no_build,
|
no_build,
|
||||||
no_binary,
|
no_binary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use uv_installer::{
|
||||||
};
|
};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
use uv_resolver::InMemoryIndex;
|
use uv_resolver::InMemoryIndex;
|
||||||
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter};
|
use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter};
|
||||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||||
|
|
@ -38,6 +38,7 @@ pub(crate) async fn pip_sync(
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
no_build_isolation: bool,
|
||||||
no_build: &NoBuild,
|
no_build: &NoBuild,
|
||||||
no_binary: &NoBinary,
|
no_binary: &NoBinary,
|
||||||
strict: bool,
|
strict: bool,
|
||||||
|
|
@ -135,6 +136,13 @@ pub(crate) async fn pip_sync(
|
||||||
// Track in-flight downloads, builds, etc., across resolutions.
|
// Track in-flight downloads, builds, etc., across resolutions.
|
||||||
let in_flight = InFlight::default();
|
let in_flight = InFlight::default();
|
||||||
|
|
||||||
|
// Determine whether to enable build isolation.
|
||||||
|
let build_isolation = if no_build_isolation {
|
||||||
|
BuildIsolation::Shared(&venv)
|
||||||
|
} else {
|
||||||
|
BuildIsolation::Isolated
|
||||||
|
};
|
||||||
|
|
||||||
// Prep the build context.
|
// Prep the build context.
|
||||||
let build_dispatch = BuildDispatch::new(
|
let build_dispatch = BuildDispatch::new(
|
||||||
&client,
|
&client,
|
||||||
|
|
@ -146,6 +154,7 @@ pub(crate) async fn pip_sync(
|
||||||
&in_flight,
|
&in_flight,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
build_isolation,
|
||||||
no_build,
|
no_build,
|
||||||
no_binary,
|
no_binary,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use uv_fs::Simplified;
|
||||||
use uv_installer::NoBinary;
|
use uv_installer::NoBinary;
|
||||||
use uv_interpreter::{find_default_python, find_requested_python, Error};
|
use uv_interpreter::{find_default_python, find_requested_python, Error};
|
||||||
use uv_resolver::{InMemoryIndex, OptionsBuilder};
|
use uv_resolver::{InMemoryIndex, OptionsBuilder};
|
||||||
use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
@ -172,6 +172,7 @@ async fn venv_impl(
|
||||||
&in_flight,
|
&in_flight,
|
||||||
SetupPyStrategy::default(),
|
SetupPyStrategy::default(),
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
BuildIsolation::Isolated,
|
||||||
&NoBuild::All,
|
&NoBuild::All,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,6 @@ pub(crate) struct PipCompileCompatArgs {
|
||||||
#[clap(long, hide = true)]
|
#[clap(long, hide = true)]
|
||||||
build_isolation: bool,
|
build_isolation: bool,
|
||||||
|
|
||||||
#[clap(long, hide = true)]
|
|
||||||
no_build_isolation: bool,
|
|
||||||
|
|
||||||
#[clap(long, hide = true)]
|
#[clap(long, hide = true)]
|
||||||
resolver: Option<Resolver>,
|
resolver: Option<Resolver>,
|
||||||
|
|
||||||
|
|
@ -117,12 +114,6 @@ impl CompatArgs for PipCompileCompatArgs {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.no_build_isolation {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"pip-compile's `--no-build-isolation` is unsupported (uv always uses build isolation)."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(resolver) = self.resolver {
|
if let Some(resolver) = self.resolver {
|
||||||
match resolver {
|
match resolver {
|
||||||
Resolver::Backtracking => {
|
Resolver::Backtracking => {
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,12 @@ struct PipCompileArgs {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
legacy_setup_py: bool,
|
legacy_setup_py: bool,
|
||||||
|
|
||||||
|
/// Disable isolation when building source distributions.
|
||||||
|
///
|
||||||
|
/// Assumes that build dependencies specified by PEP 518 are already installed.
|
||||||
|
#[clap(long)]
|
||||||
|
no_build_isolation: bool,
|
||||||
|
|
||||||
/// Don't build source distributions.
|
/// Don't build source distributions.
|
||||||
///
|
///
|
||||||
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
||||||
|
|
@ -550,6 +556,12 @@ struct PipSyncArgs {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
legacy_setup_py: bool,
|
legacy_setup_py: bool,
|
||||||
|
|
||||||
|
/// Disable isolation when building source distributions.
|
||||||
|
///
|
||||||
|
/// Assumes that build dependencies specified by PEP 518 are already installed.
|
||||||
|
#[clap(long)]
|
||||||
|
no_build_isolation: bool,
|
||||||
|
|
||||||
/// Don't build source distributions.
|
/// Don't build source distributions.
|
||||||
///
|
///
|
||||||
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
||||||
|
|
@ -790,6 +802,12 @@ struct PipInstallArgs {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
legacy_setup_py: bool,
|
legacy_setup_py: bool,
|
||||||
|
|
||||||
|
/// Disable isolation when building source distributions.
|
||||||
|
///
|
||||||
|
/// Assumes that build dependencies specified by PEP 518 are already installed.
|
||||||
|
#[clap(long)]
|
||||||
|
no_build_isolation: bool,
|
||||||
|
|
||||||
/// Don't build source distributions.
|
/// Don't build source distributions.
|
||||||
///
|
///
|
||||||
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
|
||||||
|
|
@ -1358,6 +1376,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
} else {
|
} else {
|
||||||
Connectivity::Online
|
Connectivity::Online
|
||||||
},
|
},
|
||||||
|
args.no_build_isolation,
|
||||||
&no_build,
|
&no_build,
|
||||||
args.python_version,
|
args.python_version,
|
||||||
args.exclude_newer,
|
args.exclude_newer,
|
||||||
|
|
@ -1411,6 +1430,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
Connectivity::Online
|
Connectivity::Online
|
||||||
},
|
},
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
args.no_build_isolation,
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
args.strict,
|
args.strict,
|
||||||
|
|
@ -1504,6 +1524,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
Connectivity::Online
|
Connectivity::Online
|
||||||
},
|
},
|
||||||
&config_settings,
|
&config_settings,
|
||||||
|
args.no_build_isolation,
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
args.strict,
|
args.strict,
|
||||||
|
|
|
||||||
|
|
@ -2348,3 +2348,74 @@ requires-python = "<=3.8"
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install with `--no-build-isolation`, to disable isolation during PEP 517 builds.
|
||||||
|
#[test]
|
||||||
|
fn no_build_isolation() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?;
|
||||||
|
|
||||||
|
// We expect the build to fail, because `setuptools` is not installed.
|
||||||
|
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
|
||||||
|
.chain(INSTA_FILTERS.to_vec())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("-r")
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--no-build-isolation"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to download and build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz
|
||||||
|
Caused by: Failed to build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz
|
||||||
|
Caused by: Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` with exit status: 1
|
||||||
|
--- stdout:
|
||||||
|
|
||||||
|
--- stderr:
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<string>", line 8, in <module>
|
||||||
|
ModuleNotFoundError: No module named 'setuptools'
|
||||||
|
---
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
// Install `setuptools` and `wheel`.
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("setuptools")
|
||||||
|
.arg("wheel"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
Downloaded 2 packages in [TIME]
|
||||||
|
Installed 2 packages in [TIME]
|
||||||
|
+ setuptools==68.2.2
|
||||||
|
+ wheel==0.41.3
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// We expect the build to succeed, since `setuptools` is now installed.
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("-r")
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--no-build-isolation"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
Downloaded 3 packages in [TIME]
|
||||||
|
Installed 3 packages in [TIME]
|
||||||
|
+ anyio==0.0.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz)
|
||||||
|
+ idna==3.4
|
||||||
|
+ sniffio==1.3.0
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue