Load configuration options from workspace root (#4295)

## Summary

In a workspace, we now read configuration from the workspace root.
Previously, we read configuration from the first `pyproject.toml` or
`uv.toml` file in path -- but in a workspace, that would often be the
_project_ rather than the workspace configuration.

We need to read configuration from the workspace root, rather than its
members, because we lock the workspace globally, so all configuration
applies to the workspace globally.

As part of this change, the `uv-workspace` crate has been renamed to
`uv-settings` and its purpose has been narrowed significantly (it no
longer discovers a workspace; instead, it just reads the settings from a
directory).

If a user has a `uv.toml` in their directory or in a parent directory
but is _not_ in a workspace, we will still respect that use-case as
before.

Closes #4249.
This commit is contained in:
Charlie Marsh 2024-06-13 18:26:20 -07:00 committed by GitHub
parent e0a389032f
commit cacd1a2b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 338 additions and 357 deletions

View file

@ -0,0 +1,32 @@
[package]
name = "uv-settings"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lints]
workspace = true
[dependencies]
distribution-types = { workspace = true, features = ["schemars"] }
install-wheel-rs = { workspace = true, features = ["schemars"] }
pep508_rs = { workspace = true }
pypi-types = { workspace = true }
uv-configuration = { workspace = true, features = ["schemars"] }
uv-fs = { workspace = true }
uv-normalize = { workspace = true, features = ["schemars"] }
uv-resolver = { workspace = true, features = ["schemars"] }
uv-toolchain = { workspace = true, features = ["schemars"] }
dirs-sys = { workspace = true }
fs-err = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

View file

@ -0,0 +1,202 @@
use std::num::NonZeroUsize;
use std::path::PathBuf;
use distribution_types::IndexUrl;
use install_wheel_rs::linker::LinkMode;
use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, TargetTriple};
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
use uv_toolchain::PythonVersion;
use crate::{FilesystemOptions, GlobalOptions, Options, PipOptions, ResolverInstallerOptions};
pub trait Combine {
/// Combine two values, preferring the values in `self`.
///
/// The logic should follow that of Cargo's `config.toml`:
///
/// > If a key is specified in multiple config files, the values will get merged together.
/// > Numbers, strings, and booleans will use the value in the deeper config directory taking
/// > precedence over ancestor directories, where the home directory is the lowest priority.
/// > Arrays will be joined together with higher precedence items being placed later in the
/// > merged array.
///
/// ...with one exception: we place items with higher precedence earlier in the merged array.
#[must_use]
fn combine(self, other: Self) -> Self;
}
impl Combine for Option<FilesystemOptions> {
/// Combine the options used in two [`FilesystemOptions`]s. Retains the root of `self`.
fn combine(self, other: Option<FilesystemOptions>) -> Option<FilesystemOptions> {
match (self, other) {
(Some(a), Some(b)) => Some(FilesystemOptions(
a.into_options().combine(b.into_options()),
)),
(a, b) => a.or(b),
}
}
}
impl Combine for Options {
fn combine(self, other: Options) -> Options {
Options {
globals: self.globals.combine(other.globals),
top_level: self.top_level.combine(other.top_level),
pip: self.pip.combine(other.pip),
override_dependencies: self
.override_dependencies
.combine(other.override_dependencies),
}
}
}
impl Combine for GlobalOptions {
fn combine(self, other: GlobalOptions) -> GlobalOptions {
GlobalOptions {
native_tls: self.native_tls.combine(other.native_tls),
offline: self.offline.combine(other.offline),
no_cache: self.no_cache.combine(other.no_cache),
cache_dir: self.cache_dir.combine(other.cache_dir),
preview: self.preview.combine(other.preview),
}
}
}
impl Combine for ResolverInstallerOptions {
fn combine(self, other: ResolverInstallerOptions) -> ResolverInstallerOptions {
ResolverInstallerOptions {
index_url: self.index_url.combine(other.index_url),
extra_index_url: self.extra_index_url.combine(other.extra_index_url),
no_index: self.no_index.combine(other.no_index),
find_links: self.find_links.combine(other.find_links),
index_strategy: self.index_strategy.combine(other.index_strategy),
keyring_provider: self.keyring_provider.combine(other.keyring_provider),
resolution: self.resolution.combine(other.resolution),
prerelease: self.prerelease.combine(other.prerelease),
config_settings: self.config_settings.combine(other.config_settings),
exclude_newer: self.exclude_newer.combine(other.exclude_newer),
link_mode: self.link_mode.combine(other.link_mode),
compile_bytecode: self.compile_bytecode.combine(other.compile_bytecode),
}
}
}
impl Combine for Option<PipOptions> {
fn combine(self, other: Option<PipOptions>) -> Option<PipOptions> {
match (self, other) {
(Some(a), Some(b)) => Some(a.combine(b)),
(a, b) => a.or(b),
}
}
}
impl Combine for PipOptions {
fn combine(self, other: PipOptions) -> PipOptions {
PipOptions {
python: self.python.combine(other.python),
system: self.system.combine(other.system),
break_system_packages: self
.break_system_packages
.combine(other.break_system_packages),
target: self.target.combine(other.target),
prefix: self.prefix.combine(other.prefix),
index_url: self.index_url.combine(other.index_url),
extra_index_url: self.extra_index_url.combine(other.extra_index_url),
no_index: self.no_index.combine(other.no_index),
find_links: self.find_links.combine(other.find_links),
index_strategy: self.index_strategy.combine(other.index_strategy),
keyring_provider: self.keyring_provider.combine(other.keyring_provider),
no_build: self.no_build.combine(other.no_build),
no_binary: self.no_binary.combine(other.no_binary),
only_binary: self.only_binary.combine(other.only_binary),
no_build_isolation: self.no_build_isolation.combine(other.no_build_isolation),
strict: self.strict.combine(other.strict),
extra: self.extra.combine(other.extra),
all_extras: self.all_extras.combine(other.all_extras),
no_deps: self.no_deps.combine(other.no_deps),
resolution: self.resolution.combine(other.resolution),
prerelease: self.prerelease.combine(other.prerelease),
output_file: self.output_file.combine(other.output_file),
no_strip_extras: self.no_strip_extras.combine(other.no_strip_extras),
no_annotate: self.no_annotate.combine(other.no_annotate),
no_header: self.no_header.combine(other.no_header),
custom_compile_command: self
.custom_compile_command
.combine(other.custom_compile_command),
generate_hashes: self.generate_hashes.combine(other.generate_hashes),
legacy_setup_py: self.legacy_setup_py.combine(other.legacy_setup_py),
config_settings: self.config_settings.combine(other.config_settings),
python_version: self.python_version.combine(other.python_version),
python_platform: self.python_platform.combine(other.python_platform),
exclude_newer: self.exclude_newer.combine(other.exclude_newer),
no_emit_package: self.no_emit_package.combine(other.no_emit_package),
emit_index_url: self.emit_index_url.combine(other.emit_index_url),
emit_find_links: self.emit_find_links.combine(other.emit_find_links),
emit_marker_expression: self
.emit_marker_expression
.combine(other.emit_marker_expression),
emit_index_annotation: self
.emit_index_annotation
.combine(other.emit_index_annotation),
annotation_style: self.annotation_style.combine(other.annotation_style),
link_mode: self.link_mode.combine(other.link_mode),
compile_bytecode: self.compile_bytecode.combine(other.compile_bytecode),
require_hashes: self.require_hashes.combine(other.require_hashes),
concurrent_downloads: self
.concurrent_downloads
.combine(other.concurrent_downloads),
concurrent_builds: self.concurrent_builds.combine(other.concurrent_builds),
concurrent_installs: self.concurrent_installs.combine(other.concurrent_installs),
}
}
}
macro_rules! impl_combine_or {
($name:ident) => {
impl Combine for Option<$name> {
fn combine(self, other: Option<$name>) -> Option<$name> {
self.or(other)
}
}
};
}
impl_combine_or!(AnnotationStyle);
impl_combine_or!(ExcludeNewer);
impl_combine_or!(IndexStrategy);
impl_combine_or!(IndexUrl);
impl_combine_or!(KeyringProviderType);
impl_combine_or!(LinkMode);
impl_combine_or!(NonZeroUsize);
impl_combine_or!(PathBuf);
impl_combine_or!(PreReleaseMode);
impl_combine_or!(PythonVersion);
impl_combine_or!(ResolutionMode);
impl_combine_or!(String);
impl_combine_or!(TargetTriple);
impl_combine_or!(bool);
impl<T> Combine for Option<Vec<T>> {
/// Combine two vectors by extending the vector in `self` with the vector in `other`, if they're
/// both `Some`.
fn combine(self, other: Option<Vec<T>>) -> Option<Vec<T>> {
match (self, other) {
(Some(mut a), Some(b)) => {
a.extend(b);
Some(a)
}
(a, b) => a.or(b),
}
}
}
impl Combine for Option<ConfigSettings> {
/// Combine two maps by merging the map in `self` with the map in `other`, if they're both
/// `Some`.
fn combine(self, other: Option<ConfigSettings>) -> Option<ConfigSettings> {
match (self, other) {
(Some(a), Some(b)) => Some(a.merge(b)),
(a, b) => a.or(b),
}
}
}

View file

@ -0,0 +1,167 @@
use std::ops::Deref;
use std::path::{Path, PathBuf};
use tracing::debug;
use uv_fs::Simplified;
pub use crate::combine::*;
pub use crate::settings::*;
mod combine;
mod settings;
/// The [`Options`] as loaded from a configuration file on disk.
#[derive(Debug, Clone)]
pub struct FilesystemOptions(Options);
impl FilesystemOptions {
/// Convert the [`FilesystemOptions`] into [`Options`].
pub fn into_options(self) -> Options {
self.0
}
}
impl Deref for FilesystemOptions {
type Target = Options;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FilesystemOptions {
/// Load the user [`FilesystemOptions`].
pub fn user() -> Result<Option<Self>, Error> {
let Some(dir) = config_dir() else {
return Ok(None);
};
let root = dir.join("uv");
let file = root.join("uv.toml");
debug!("Loading user configuration from: `{}`", file.display());
match read_file(&file) {
Ok(options) => Ok(Some(Self(options))),
Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(_) if !dir.is_dir() => {
// Ex) `XDG_CONFIG_HOME=/dev/null`
debug!(
"User configuration directory `{}` does not exist or is not a directory",
dir.display()
);
Ok(None)
}
Err(err) => Err(err),
}
}
/// Find the [`FilesystemOptions`] for the given path.
///
/// The search starts at the given path and goes up the directory tree until a `uv.toml` file is
/// found.
pub fn find(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
for ancestor in path.as_ref().ancestors() {
// Read a `uv.toml` file in the current directory.
let path = ancestor.join("uv.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(Self(options)));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
}
Ok(None)
}
/// Load a [`FilesystemOptions`] from a directory, preferring a `uv.toml` file over a
/// `pyproject.toml` file.
pub fn from_directory(dir: impl AsRef<Path>) -> Result<Option<Self>, Error> {
// Read a `uv.toml` file in the current directory.
let path = dir.as_ref().join("uv.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(Self(options)));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
// Read a `pyproject.toml` file in the current directory.
let path = dir.as_ref().join("pyproject.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
// Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
let pyproject: PyProjectToml = toml::from_str(&content)
.map_err(|err| Error::PyprojectToml(path.user_display().to_string(), err))?;
let Some(tool) = pyproject.tool else {
return Ok(None);
};
let Some(options) = tool.uv else {
return Ok(None);
};
debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(Self(options)));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
Ok(None)
}
/// Load a [`FilesystemOptions`] from a `uv.toml` file.
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
Ok(Self(read_file(path.as_ref())?))
}
}
/// Returns the path to the user configuration directory.
///
/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the
/// `XDG_CONFIG_HOME` environment variable on both Linux _and_ macOS, rather than the
/// `Application Support` directory on macOS.
fn config_dir() -> Option<PathBuf> {
// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming
#[cfg(windows)]
{
dirs_sys::known_folder_roaming_app_data()
}
// On Linux and macOS, use, e.g., /home/alice/.config.
#[cfg(not(windows))]
{
std::env::var_os("XDG_CONFIG_HOME")
.and_then(dirs_sys::is_absolute_path)
.or_else(|| dirs_sys::home_dir().map(|path| path.join(".config")))
}
}
/// Load [`Options`] from a `uv.toml` file.
fn read_file(path: &Path) -> Result<Options, Error> {
let content = fs_err::read_to_string(path)?;
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
Ok(options)
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to parse: `{0}`")]
PyprojectToml(String, #[source] toml::de::Error),
#[error("Failed to parse: `{0}`")]
UvToml(String, #[source] toml::de::Error),
}

View file

@ -0,0 +1,287 @@
use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf};
use serde::Deserialize;
use distribution_types::{FlatIndexLocation, IndexUrl};
use install_wheel_rs::linker::LinkMode;
use pypi_types::VerbatimParsedUrl;
use uv_configuration::{
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
};
use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
use uv_toolchain::PythonVersion;
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
pub(crate) struct PyProjectToml {
pub(crate) tool: Option<Tools>,
}
/// A `[tool]` section.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
pub(crate) struct Tools {
pub(crate) uv: Option<Options>,
}
/// A `[tool.uv]` section.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
#[serde(flatten)]
pub globals: GlobalOptions,
#[serde(flatten)]
pub top_level: ResolverInstallerOptions,
pub pip: Option<PipOptions>,
#[cfg_attr(
feature = "schemars",
schemars(
with = "Option<Vec<String>>",
description = "PEP 508 style requirements, e.g. `flask==3.0.0`, or `black @ https://...`."
)
)]
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
}
/// Global settings, relevant to all invocations.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GlobalOptions {
pub native_tls: Option<bool>,
pub offline: Option<bool>,
pub no_cache: Option<bool>,
pub cache_dir: Option<PathBuf>,
pub preview: Option<bool>,
}
/// Settings relevant to all installer operations.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct InstallerOptions {
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<FlatIndexLocation>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub config_settings: Option<ConfigSettings>,
pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>,
}
/// Settings relevant to all resolver operations.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ResolverOptions {
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<FlatIndexLocation>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PreReleaseMode>,
pub config_settings: Option<ConfigSettings>,
pub exclude_newer: Option<ExcludeNewer>,
pub link_mode: Option<LinkMode>,
}
/// Shared settings, relevant to all operations that must resolve and install dependencies. The
/// union of [`InstallerOptions`] and [`ResolverOptions`].
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ResolverInstallerOptions {
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<FlatIndexLocation>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PreReleaseMode>,
pub config_settings: Option<ConfigSettings>,
pub exclude_newer: Option<ExcludeNewer>,
pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>,
}
/// A `[tool.uv.pip]` section.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PipOptions {
pub python: Option<String>,
pub system: Option<bool>,
pub break_system_packages: Option<bool>,
pub target: Option<PathBuf>,
pub prefix: Option<PathBuf>,
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<FlatIndexLocation>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub no_build: Option<bool>,
pub no_binary: Option<Vec<PackageNameSpecifier>>,
pub only_binary: Option<Vec<PackageNameSpecifier>>,
pub no_build_isolation: Option<bool>,
pub strict: Option<bool>,
pub extra: Option<Vec<ExtraName>>,
pub all_extras: Option<bool>,
pub no_deps: Option<bool>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PreReleaseMode>,
pub output_file: Option<PathBuf>,
pub no_strip_extras: Option<bool>,
pub no_annotate: Option<bool>,
pub no_header: Option<bool>,
pub custom_compile_command: Option<String>,
pub generate_hashes: Option<bool>,
pub legacy_setup_py: Option<bool>,
pub config_settings: Option<ConfigSettings>,
pub python_version: Option<PythonVersion>,
pub python_platform: Option<TargetTriple>,
pub exclude_newer: Option<ExcludeNewer>,
pub no_emit_package: Option<Vec<PackageName>>,
pub emit_index_url: Option<bool>,
pub emit_find_links: Option<bool>,
pub emit_marker_expression: Option<bool>,
pub emit_index_annotation: Option<bool>,
pub annotation_style: Option<AnnotationStyle>,
pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>,
pub require_hashes: Option<bool>,
pub concurrent_downloads: Option<NonZeroUsize>,
pub concurrent_builds: Option<NonZeroUsize>,
pub concurrent_installs: Option<NonZeroUsize>,
}
impl Options {
/// Return the `pip` section, with any top-level options merged in. If options are repeated
/// between the top-level and the `pip` section, the `pip` options are preferred.
///
/// For example, prefers `tool.uv.pip.index-url` over `tool.uv.index-url`.
pub fn pip(self) -> PipOptions {
let PipOptions {
python,
system,
break_system_packages,
target,
prefix,
index_url,
extra_index_url,
no_index,
find_links,
index_strategy,
keyring_provider,
no_build,
no_binary,
only_binary,
no_build_isolation,
strict,
extra,
all_extras,
no_deps,
resolution,
prerelease,
output_file,
no_strip_extras,
no_annotate,
no_header,
custom_compile_command,
generate_hashes,
legacy_setup_py,
config_settings,
python_version,
python_platform,
exclude_newer,
no_emit_package,
emit_index_url,
emit_find_links,
emit_marker_expression,
emit_index_annotation,
annotation_style,
link_mode,
compile_bytecode,
require_hashes,
concurrent_builds,
concurrent_downloads,
concurrent_installs,
} = self.pip.unwrap_or_default();
let ResolverInstallerOptions {
index_url: top_level_index_url,
extra_index_url: top_level_extra_index_url,
no_index: top_level_no_index,
find_links: top_level_find_links,
index_strategy: top_level_index_strategy,
keyring_provider: top_level_keyring_provider,
resolution: top_level_resolution,
prerelease: top_level_prerelease,
config_settings: top_level_config_settings,
exclude_newer: top_level_exclude_newer,
link_mode: top_level_link_mode,
compile_bytecode: top_level_compile_bytecode,
} = self.top_level;
PipOptions {
python,
system,
break_system_packages,
target,
prefix,
index_url: index_url.or(top_level_index_url),
extra_index_url: extra_index_url.or(top_level_extra_index_url),
no_index: no_index.or(top_level_no_index),
find_links: find_links.or(top_level_find_links),
index_strategy: index_strategy.or(top_level_index_strategy),
keyring_provider: keyring_provider.or(top_level_keyring_provider),
no_build,
no_binary,
only_binary,
no_build_isolation,
strict,
extra,
all_extras,
no_deps,
resolution: resolution.or(top_level_resolution),
prerelease: prerelease.or(top_level_prerelease),
output_file,
no_strip_extras,
no_annotate,
no_header,
custom_compile_command,
generate_hashes,
legacy_setup_py,
config_settings: config_settings.or(top_level_config_settings),
python_version,
python_platform,
exclude_newer: exclude_newer.or(top_level_exclude_newer),
no_emit_package,
emit_index_url,
emit_find_links,
emit_marker_expression,
emit_index_annotation,
annotation_style,
link_mode: link_mode.or(top_level_link_mode),
compile_bytecode: compile_bytecode.or(top_level_compile_bytecode),
require_hashes,
concurrent_builds,
concurrent_downloads,
concurrent_installs,
}
}
}