mirror of https://github.com/astral-sh/uv
Add SBOM export support (#16523)
Co-authored-by: Will Rollason <william.rollason@snyk.io>
This commit is contained in:
parent
7d8634bf35
commit
fd7e6d0a05
|
|
@ -1108,6 +1108,41 @@ dependencies = [
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cyclonedx-bom"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce2ec98a191e17f63b92b132f6852462de9eaee03ca8dbf2df401b9fd809bcac"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"cyclonedx-bom-macros",
|
||||||
|
"fluent-uri",
|
||||||
|
"indexmap",
|
||||||
|
"once_cell",
|
||||||
|
"ordered-float",
|
||||||
|
"purl",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"spdx 0.10.9",
|
||||||
|
"strum",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
"uuid",
|
||||||
|
"xml-rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cyclonedx-bom-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c50341f21df64b412b4f917e34b7aa786c092d64f3f905f478cb76950c7e980c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
|
|
@ -1475,6 +1510,15 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-uri"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
@ -2837,6 +2881,15 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-multimap"
|
name = "ordered-multimap"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
@ -3214,6 +3267,17 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "purl"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad"
|
||||||
|
dependencies = [
|
||||||
|
"hex",
|
||||||
|
"percent-encoding",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.3"
|
version = "0.38.3"
|
||||||
|
|
@ -4292,6 +4356,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spdx"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spdx"
|
name = "spdx"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
@ -4338,6 +4411,28 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -4665,10 +4760,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4677,6 +4774,16 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-keccak"
|
name = "tiny-keccak"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
|
@ -5509,7 +5616,7 @@ dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"spdx",
|
"spdx 0.12.0",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
|
@ -6532,6 +6639,7 @@ dependencies = [
|
||||||
"arcstr",
|
"arcstr",
|
||||||
"astral-pubgrub",
|
"astral-pubgrub",
|
||||||
"clap",
|
"clap",
|
||||||
|
"cyclonedx-bom",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"either",
|
"either",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
|
|
@ -6542,6 +6650,7 @@ dependencies = [
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"jiff",
|
"jiff",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
|
"percent-encoding",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
|
@ -6574,6 +6683,7 @@ dependencies = [
|
||||||
"uv-pep440",
|
"uv-pep440",
|
||||||
"uv-pep508",
|
"uv-pep508",
|
||||||
"uv-platform-tags",
|
"uv-platform-tags",
|
||||||
|
"uv-preview",
|
||||||
"uv-pypi-types",
|
"uv-pypi-types",
|
||||||
"uv-python",
|
"uv-python",
|
||||||
"uv-redacted",
|
"uv-redacted",
|
||||||
|
|
@ -6582,6 +6692,7 @@ dependencies = [
|
||||||
"uv-static",
|
"uv-static",
|
||||||
"uv-torch",
|
"uv-torch",
|
||||||
"uv-types",
|
"uv-types",
|
||||||
|
"uv-version",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
"uv-workspace",
|
"uv-workspace",
|
||||||
]
|
]
|
||||||
|
|
@ -7515,6 +7626,12 @@ dependencies = [
|
||||||
"rustix 1.0.8",
|
"rustix 1.0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml-rs"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xmlparser"
|
name = "xmlparser"
|
||||||
version = "0.13.6"
|
version = "0.13.6"
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ configparser = { version = "3.1.0" }
|
||||||
console = { version = "0.16.0", default-features = false, features = ["std"] }
|
console = { version = "0.16.0", default-features = false, features = ["std"] }
|
||||||
csv = { version = "1.3.0" }
|
csv = { version = "1.3.0" }
|
||||||
ctrlc = { version = "3.4.5" }
|
ctrlc = { version = "3.4.5" }
|
||||||
|
cyclonedx-bom = { version = "0.8.0" }
|
||||||
dashmap = { version = "6.1.0" }
|
dashmap = { version = "6.1.0" }
|
||||||
data-encoding = { version = "2.6.0" }
|
data-encoding = { version = "2.6.0" }
|
||||||
dotenvy = { version = "0.15.7" }
|
dotenvy = { version = "0.15.7" }
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ use clap::{Args, Parser, Subcommand};
|
||||||
use uv_auth::Service;
|
use uv_auth::Service;
|
||||||
use uv_cache::CacheArgs;
|
use uv_cache::CacheArgs;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend,
|
ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, PipCompileFormat,
|
||||||
TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
|
ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
|
||||||
};
|
};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex,
|
ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex,
|
||||||
|
|
@ -1442,7 +1442,7 @@ pub struct PipCompileArgs {
|
||||||
/// uv will infer the output format from the file extension of the output file, if
|
/// uv will infer the output format from the file extension of the output file, if
|
||||||
/// provided. Otherwise, defaults to `requirements.txt`.
|
/// provided. Otherwise, defaults to `requirements.txt`.
|
||||||
#[arg(long, value_enum)]
|
#[arg(long, value_enum)]
|
||||||
pub format: Option<ExportFormat>,
|
pub format: Option<PipCompileFormat>,
|
||||||
|
|
||||||
/// Include extras in the output file.
|
/// Include extras in the output file.
|
||||||
///
|
///
|
||||||
|
|
@ -4543,9 +4543,10 @@ pub struct TreeArgs {
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct ExportArgs {
|
pub struct ExportArgs {
|
||||||
|
#[allow(clippy::doc_markdown)]
|
||||||
/// The format to which `uv.lock` should be exported.
|
/// The format to which `uv.lock` should be exported.
|
||||||
///
|
///
|
||||||
/// Supports both `requirements.txt` and `pylock.toml` (PEP 751) output formats.
|
/// Supports `requirements.txt`, `pylock.toml` (PEP 751) and CycloneDX v1.5 JSON output formats.
|
||||||
///
|
///
|
||||||
/// uv will infer the output format from the file extension of the output file, if
|
/// uv will infer the output format from the file extension of the output file, if
|
||||||
/// provided. Otherwise, defaults to `requirements.txt`.
|
/// provided. Otherwise, defaults to `requirements.txt`.
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,30 @@ pub enum ExportFormat {
|
||||||
#[serde(rename = "pylock.toml", alias = "pylock-toml")]
|
#[serde(rename = "pylock.toml", alias = "pylock-toml")]
|
||||||
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
|
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
|
||||||
PylockToml,
|
PylockToml,
|
||||||
|
/// Export in `CycloneDX` v1.5 JSON format.
|
||||||
|
#[serde(rename = "cyclonedx1.5")]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(name = "cyclonedx1.5", alias = "cyclonedx1.5+json")
|
||||||
|
)]
|
||||||
|
CycloneDX1_5,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output format to use in `uv pip compile`.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
pub enum PipCompileFormat {
|
||||||
|
/// Export in `requirements.txt` format.
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "requirements.txt", alias = "requirements-txt")]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(name = "requirements.txt", alias = "requirements-txt")
|
||||||
|
)]
|
||||||
|
RequirementsTxt,
|
||||||
|
/// Export in `pylock.toml` format.
|
||||||
|
#[serde(rename = "pylock.toml", alias = "pylock-toml")]
|
||||||
|
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
|
||||||
|
PylockToml,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ bitflags::bitflags! {
|
||||||
const WORKSPACE_METADATA = 1 << 13;
|
const WORKSPACE_METADATA = 1 << 13;
|
||||||
const WORKSPACE_DIR = 1 << 14;
|
const WORKSPACE_DIR = 1 << 14;
|
||||||
const WORKSPACE_LIST = 1 << 15;
|
const WORKSPACE_LIST = 1 << 15;
|
||||||
|
const SBOM_EXPORT = 1 << 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +51,7 @@ impl PreviewFeatures {
|
||||||
Self::WORKSPACE_METADATA => "workspace-metadata",
|
Self::WORKSPACE_METADATA => "workspace-metadata",
|
||||||
Self::WORKSPACE_DIR => "workspace-dir",
|
Self::WORKSPACE_DIR => "workspace-dir",
|
||||||
Self::WORKSPACE_LIST => "workspace-list",
|
Self::WORKSPACE_LIST => "workspace-list",
|
||||||
|
Self::SBOM_EXPORT => "sbom-export",
|
||||||
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
|
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +105,7 @@ impl FromStr for PreviewFeatures {
|
||||||
"workspace-metadata" => Self::WORKSPACE_METADATA,
|
"workspace-metadata" => Self::WORKSPACE_METADATA,
|
||||||
"workspace-dir" => Self::WORKSPACE_DIR,
|
"workspace-dir" => Self::WORKSPACE_DIR,
|
||||||
"workspace-list" => Self::WORKSPACE_LIST,
|
"workspace-list" => Self::WORKSPACE_LIST,
|
||||||
|
"sbom-export" => Self::SBOM_EXPORT,
|
||||||
_ => {
|
_ => {
|
||||||
warn_user_once!("Unknown preview feature: `{part}`");
|
warn_user_once!("Unknown preview feature: `{part}`");
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -278,6 +281,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
|
assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
|
||||||
assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint");
|
assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint");
|
||||||
|
assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ uv-once-map = { workspace = true }
|
||||||
uv-pep440 = { workspace = true }
|
uv-pep440 = { workspace = true }
|
||||||
uv-pep508 = { workspace = true }
|
uv-pep508 = { workspace = true }
|
||||||
uv-platform-tags = { workspace = true }
|
uv-platform-tags = { workspace = true }
|
||||||
|
uv-preview = { workspace = true }
|
||||||
uv-pypi-types = { workspace = true }
|
uv-pypi-types = { workspace = true }
|
||||||
uv-python = { workspace = true }
|
uv-python = { workspace = true }
|
||||||
uv-redacted = { workspace = true }
|
uv-redacted = { workspace = true }
|
||||||
|
|
@ -41,11 +42,13 @@ uv-small-str = { workspace = true }
|
||||||
uv-static = { workspace = true }
|
uv-static = { workspace = true }
|
||||||
uv-torch = { workspace = true }
|
uv-torch = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
|
uv-version = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
uv-workspace = { workspace = true }
|
uv-workspace = { workspace = true }
|
||||||
|
|
||||||
arcstr = { workspace = true }
|
arcstr = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive"], optional = true }
|
clap = { workspace = true, features = ["derive"], optional = true }
|
||||||
|
cyclonedx-bom = { workspace = true }
|
||||||
dashmap = { workspace = true }
|
dashmap = { workspace = true }
|
||||||
either = { workspace = true }
|
either = { workspace = true }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
|
|
@ -55,6 +58,7 @@ indexmap = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
jiff = { workspace = true, features = ["serde"] }
|
jiff = { workspace = true, features = ["serde"] }
|
||||||
owo-colors = { workspace = true }
|
owo-colors = { workspace = true }
|
||||||
|
percent-encoding = { workspace = true }
|
||||||
petgraph = { workspace = true }
|
petgraph = { workspace = true }
|
||||||
pubgrub = { workspace = true }
|
pubgrub = { workspace = true }
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub use fork_strategy::ForkStrategy;
|
||||||
pub use lock::{
|
pub use lock::{
|
||||||
Installable, Lock, LockError, LockVersion, Package, PackageMap, PylockToml,
|
Installable, Lock, LockError, LockVersion, Package, PackageMap, PylockToml,
|
||||||
PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay,
|
PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay,
|
||||||
VERSION,
|
VERSION, cyclonedx_json,
|
||||||
};
|
};
|
||||||
pub use manifest::Manifest;
|
pub use manifest::Manifest;
|
||||||
pub use options::{Flexibility, Options, OptionsBuilder};
|
pub use options::{Flexibility, Options, OptionsBuilder};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use cyclonedx_bom::models::component::Classification;
|
||||||
|
use cyclonedx_bom::models::dependency::{Dependencies, Dependency};
|
||||||
|
use cyclonedx_bom::models::metadata::Metadata;
|
||||||
|
use cyclonedx_bom::models::property::{Properties, Property};
|
||||||
|
use cyclonedx_bom::models::tool::{Tool, Tools};
|
||||||
|
use cyclonedx_bom::prelude::{Bom, Component, Components, NormalizedString};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
|
use uv_configuration::{
|
||||||
|
DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults, InstallOptions,
|
||||||
|
};
|
||||||
|
use uv_fs::PortablePath;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_pep508::MarkerTree;
|
||||||
|
use uv_preview::{Preview, PreviewFeatures};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
use crate::lock::export::{ExportableRequirement, ExportableRequirements};
|
||||||
|
use crate::lock::{LockErrorKind, Package, PackageId, RegistrySource, Source};
|
||||||
|
use crate::{Installable, LockError};
|
||||||
|
|
||||||
|
/// Character set for percent-encoding PURL components, copied from packageurl.rs (<https://github.com/scm-rs/packageurl.rs/blob/a725aa0ab332934c350641508017eb09ddfa0813/src/purl.rs#L18>).
|
||||||
|
const PURL_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
|
.add(b' ')
|
||||||
|
.add(b'"')
|
||||||
|
.add(b'#')
|
||||||
|
.add(b'%')
|
||||||
|
.add(b'<')
|
||||||
|
.add(b'>')
|
||||||
|
.add(b'`')
|
||||||
|
.add(b'?')
|
||||||
|
.add(b'{')
|
||||||
|
.add(b'}')
|
||||||
|
.add(b';')
|
||||||
|
.add(b'=')
|
||||||
|
.add(b'+')
|
||||||
|
.add(b'@')
|
||||||
|
.add(b'\\')
|
||||||
|
.add(b'[')
|
||||||
|
.add(b']')
|
||||||
|
.add(b'^')
|
||||||
|
.add(b'|');
|
||||||
|
|
||||||
|
/// Creates `CycloneDX` components, registering them in a `HashMap` so that they can be retrieved by `PackageId`.
|
||||||
|
/// Also ensures uniqueness when generating bom-refs by using a numeric prefix which is incremented for each component.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ComponentBuilder<'a> {
|
||||||
|
id_counter: usize, // Used as prefix in bom-ref generation, to ensure uniqueness
|
||||||
|
package_to_component_map: HashMap<&'a PackageId, Component>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ComponentBuilder<'a> {
|
||||||
|
/// Creates a bom-ref string in the format "{package_name}-{id}@{version}" or "{package_name}-{id}" if no version is provided.
|
||||||
|
fn create_bom_ref(&mut self, name: &str, version: Option<&str>) -> String {
|
||||||
|
self.id_counter += 1;
|
||||||
|
let id = self.id_counter;
|
||||||
|
if let Some(version) = version {
|
||||||
|
format!("{name}-{id}@{version}")
|
||||||
|
} else {
|
||||||
|
format!("{name}-{id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract version string from a package.
|
||||||
|
fn get_version_string(package: &Package) -> Option<String> {
|
||||||
|
package
|
||||||
|
.id
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(std::string::ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract package name string from a package.
|
||||||
|
fn get_package_name(package: &Package) -> &str {
|
||||||
|
package.id.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a Package URL (purl) from a package. Returns `None` for local sources.
|
||||||
|
fn create_purl(package: &Package) -> Option<String> {
|
||||||
|
let name = percent_encode(Self::get_package_name(package).as_bytes(), PURL_ENCODE_SET);
|
||||||
|
|
||||||
|
let version = Self::get_version_string(package)
|
||||||
|
.map(|v| format!("@{}", percent_encode(v.as_bytes(), PURL_ENCODE_SET)))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let (purl_type, qualifiers) = match &package.id.source {
|
||||||
|
// By convention all Python packages use the "pypi" purl type, regardless of their source. For packages
|
||||||
|
// from non-default repositories, we add a qualifier to indicate their source explicitly.
|
||||||
|
// See the specs at
|
||||||
|
// https://github.com/package-url/purl-spec/blob/9041aa7/types/pypi-definition.json
|
||||||
|
// and https://github.com/package-url/purl-spec/blob/9041aa7/purl-specification.md
|
||||||
|
Source::Registry(registry_source) => {
|
||||||
|
let qualifiers = match registry_source {
|
||||||
|
RegistrySource::Url(url) => {
|
||||||
|
// Only add repository_url qualifier for non-default registries
|
||||||
|
if !url.as_ref().starts_with("https://pypi.org/") {
|
||||||
|
vec![("repository_url", url.as_ref())]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegistrySource::Path(_) => vec![],
|
||||||
|
};
|
||||||
|
("pypi", qualifiers)
|
||||||
|
}
|
||||||
|
Source::Git(url, _) => ("pypi", vec![("vcs_url", url.as_ref())]),
|
||||||
|
Source::Direct(url, _) => ("pypi", vec![("download_url", url.as_ref())]),
|
||||||
|
// No purl for local sources
|
||||||
|
Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let qualifiers = if qualifiers.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
Self::format_qualifiers(&qualifiers)
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(format!("pkg:{purl_type}/{name}{version}{qualifiers}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String {
|
||||||
|
let joined_qualifiers = qualifiers
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| {
|
||||||
|
format!(
|
||||||
|
"{key}={}",
|
||||||
|
percent_encode(value.as_bytes(), PURL_ENCODE_SET)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join("&");
|
||||||
|
format!("?{joined_qualifiers}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_component(
|
||||||
|
&mut self,
|
||||||
|
package: &'a Package,
|
||||||
|
package_type: PackageType,
|
||||||
|
marker: Option<&MarkerTree>,
|
||||||
|
) -> Component {
|
||||||
|
let component = self.create_component_from_package(package, package_type, marker);
|
||||||
|
self.package_to_component_map
|
||||||
|
.insert(&package.id, component.clone());
|
||||||
|
component
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_synthetic_root_component(&mut self, root: Option<&Package>) -> Component {
|
||||||
|
let name = root.map(Self::get_package_name).unwrap_or("uv-workspace");
|
||||||
|
let bom_ref = self.create_bom_ref(name, None);
|
||||||
|
|
||||||
|
// No need to register as we manually add dependencies in `if all_packages` check in `from_lock`
|
||||||
|
Component {
|
||||||
|
component_type: Classification::Library,
|
||||||
|
name: NormalizedString::new(name),
|
||||||
|
version: None,
|
||||||
|
bom_ref: Some(bom_ref),
|
||||||
|
purl: None,
|
||||||
|
mime_type: None,
|
||||||
|
supplier: None,
|
||||||
|
author: None,
|
||||||
|
publisher: None,
|
||||||
|
group: None,
|
||||||
|
description: None,
|
||||||
|
scope: None,
|
||||||
|
hashes: None,
|
||||||
|
licenses: None,
|
||||||
|
copyright: None,
|
||||||
|
cpe: None,
|
||||||
|
swid: None,
|
||||||
|
modified: None,
|
||||||
|
pedigree: None,
|
||||||
|
external_references: None,
|
||||||
|
properties: None,
|
||||||
|
components: None,
|
||||||
|
evidence: None,
|
||||||
|
signature: None,
|
||||||
|
model_card: None,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_component_from_package(
|
||||||
|
&mut self,
|
||||||
|
package: &Package,
|
||||||
|
package_type: PackageType,
|
||||||
|
marker: Option<&MarkerTree>,
|
||||||
|
) -> Component {
|
||||||
|
let name = Self::get_package_name(package);
|
||||||
|
let version = Self::get_version_string(package);
|
||||||
|
let bom_ref = self.create_bom_ref(name, version.as_deref());
|
||||||
|
let purl = Self::create_purl(package).and_then(|purl_string| purl_string.parse().ok());
|
||||||
|
let mut properties = vec![];
|
||||||
|
|
||||||
|
match package_type {
|
||||||
|
PackageType::Workspace(path) => {
|
||||||
|
properties.push(Property::new(
|
||||||
|
"uv:workspace:path",
|
||||||
|
&PortablePath::from(path).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
PackageType::Root | PackageType::Dependency => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) {
|
||||||
|
properties.push(Property::new(
|
||||||
|
"uv:package:marker",
|
||||||
|
&marker_contents.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
component_type: Classification::Library,
|
||||||
|
name: NormalizedString::new(name),
|
||||||
|
version: version.as_deref().map(NormalizedString::new),
|
||||||
|
bom_ref: Some(bom_ref),
|
||||||
|
purl,
|
||||||
|
mime_type: None,
|
||||||
|
supplier: None,
|
||||||
|
author: None,
|
||||||
|
publisher: None,
|
||||||
|
group: None,
|
||||||
|
description: None,
|
||||||
|
scope: None,
|
||||||
|
hashes: None,
|
||||||
|
licenses: None,
|
||||||
|
copyright: None,
|
||||||
|
cpe: None,
|
||||||
|
swid: None,
|
||||||
|
modified: None,
|
||||||
|
pedigree: None,
|
||||||
|
external_references: None,
|
||||||
|
properties: if !properties.is_empty() {
|
||||||
|
Some(Properties(properties))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
components: None,
|
||||||
|
evidence: None,
|
||||||
|
signature: None,
|
||||||
|
model_card: None,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_component(&self, id: &PackageId) -> Option<&Component> {
|
||||||
|
self.package_to_component_map.get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_lock<'lock>(
|
||||||
|
target: &impl Installable<'lock>,
|
||||||
|
prune: &[PackageName],
|
||||||
|
extras: &ExtrasSpecificationWithDefaults,
|
||||||
|
groups: &DependencyGroupsWithDefaults,
|
||||||
|
annotate: bool,
|
||||||
|
install_options: &'lock InstallOptions,
|
||||||
|
preview: Preview,
|
||||||
|
all_packages: bool,
|
||||||
|
) -> Result<Bom, LockError> {
|
||||||
|
if !preview.is_enabled(PreviewFeatures::SBOM_EXPORT) {
|
||||||
|
warn_user!(
|
||||||
|
"`uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
|
||||||
|
PreviewFeatures::SBOM_EXPORT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the packages from the lock file.
|
||||||
|
let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
|
||||||
|
target,
|
||||||
|
prune,
|
||||||
|
extras,
|
||||||
|
groups,
|
||||||
|
annotate,
|
||||||
|
install_options,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
nodes.sort_unstable_by_key(|node| &node.package.id);
|
||||||
|
|
||||||
|
// CycloneDX requires exactly one root component in `metadata.component`.
|
||||||
|
let root = match target.roots().collect::<Vec<_>>().as_slice() {
|
||||||
|
// Single root: use it directly
|
||||||
|
[single_root] => nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| &node.package.id.name == *single_root)
|
||||||
|
.map(|node| node.package),
|
||||||
|
// Multiple roots or no roots: use fallback
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
.or_else(|| target.lock().root()); // Fallback to project root
|
||||||
|
|
||||||
|
let mut component_builder = ComponentBuilder::default();
|
||||||
|
|
||||||
|
let mut metadata = Metadata {
|
||||||
|
component: root
|
||||||
|
.map(|package| component_builder.create_component(package, PackageType::Root, None)),
|
||||||
|
timestamp: cyclonedx_bom::prelude::DateTime::now().ok(),
|
||||||
|
tools: Some(Tools::List(vec![Tool {
|
||||||
|
vendor: Some(NormalizedString::new("Astral Software Inc.")),
|
||||||
|
name: Some(NormalizedString::new("uv")),
|
||||||
|
version: Some(NormalizedString::new(uv_version::version())),
|
||||||
|
hashes: None,
|
||||||
|
external_references: None,
|
||||||
|
}])),
|
||||||
|
..Metadata::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let workspace_member_ids = nodes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|node| {
|
||||||
|
if target.lock().members().contains(&node.package.id.name) {
|
||||||
|
Some(&node.package.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<FxHashSet<_>>();
|
||||||
|
|
||||||
|
let mut components = nodes
|
||||||
|
.iter()
|
||||||
|
.filter(|node| root.is_none_or(|root_pkg| root_pkg.id != node.package.id)) // Filter out root package as this is included in `metadata`
|
||||||
|
.map(|node| {
|
||||||
|
let package_type = if workspace_member_ids.contains(&node.package.id) {
|
||||||
|
let path = match &node.package.id.source {
|
||||||
|
Source::Path(path)
|
||||||
|
| Source::Directory(path)
|
||||||
|
| Source::Editable(path)
|
||||||
|
| Source::Virtual(path) => path,
|
||||||
|
Source::Registry(_) | Source::Git(_, _) | Source::Direct(_, _) => {
|
||||||
|
// Workspace packages should always be local dependencies
|
||||||
|
return Err(LockErrorKind::NonLocalWorkspaceMember {
|
||||||
|
id: node.package.id.clone(),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PackageType::Workspace(path)
|
||||||
|
} else {
|
||||||
|
PackageType::Dependency
|
||||||
|
};
|
||||||
|
Ok(component_builder.create_component(node.package, package_type, Some(&node.marker)))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, LockError>>()?;
|
||||||
|
|
||||||
|
let mut dependencies = create_dependencies(&nodes, &component_builder);
|
||||||
|
|
||||||
|
// With `--all-packages`, use synthetic root which depends on workspace root and all workspace members.
|
||||||
|
// This ensures that we don't have any dangling components resulting from workspace packages not depended on by the workspace root.
|
||||||
|
if all_packages {
|
||||||
|
let synthetic_root = component_builder.create_synthetic_root_component(root);
|
||||||
|
let synthetic_root_bom_ref = synthetic_root
|
||||||
|
.bom_ref
|
||||||
|
.clone()
|
||||||
|
.expect("bom-ref should always exist");
|
||||||
|
let workspace_root = metadata.component.replace(synthetic_root);
|
||||||
|
|
||||||
|
if let Some(workspace_root) = workspace_root {
|
||||||
|
components.push(workspace_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies.push(Dependency {
|
||||||
|
dependency_ref: synthetic_root_bom_ref,
|
||||||
|
dependencies: workspace_member_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| component_builder.get_component(c))
|
||||||
|
.map(|c| c.bom_ref.clone().expect("bom-ref should always exist"))
|
||||||
|
.sorted_unstable()
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let bom = Bom {
|
||||||
|
metadata: Some(metadata),
|
||||||
|
components: Some(Components(components)),
|
||||||
|
dependencies: Some(Dependencies(dependencies)),
|
||||||
|
..Bom::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(bom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dependencies(
|
||||||
|
nodes: &[ExportableRequirement<'_>],
|
||||||
|
component_builder: &ComponentBuilder,
|
||||||
|
) -> Vec<Dependency> {
|
||||||
|
nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node| {
|
||||||
|
let component = component_builder
|
||||||
|
.get_component(&node.package.id)
|
||||||
|
.expect("All nodes should have been added to map");
|
||||||
|
|
||||||
|
let immediate_deps = &node.package.dependencies;
|
||||||
|
let optional_deps = node.package.optional_dependencies.values().flatten();
|
||||||
|
let dep_groups = node.package.dependency_groups.values().flatten();
|
||||||
|
|
||||||
|
let package_deps = immediate_deps
|
||||||
|
.iter()
|
||||||
|
.chain(optional_deps)
|
||||||
|
.chain(dep_groups)
|
||||||
|
.filter_map(|dep| component_builder.get_component(&dep.package_id));
|
||||||
|
|
||||||
|
let bom_refs = package_deps
|
||||||
|
.map(|p| p.bom_ref.clone().expect("bom-ref should always exist"))
|
||||||
|
.sorted_unstable()
|
||||||
|
.unique()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Dependency {
|
||||||
|
dependency_ref: component
|
||||||
|
.bom_ref
|
||||||
|
.clone()
|
||||||
|
.expect("bom-ref should always exist"),
|
||||||
|
dependencies: bom_refs,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
enum PackageType<'a> {
|
||||||
|
Root,
|
||||||
|
Workspace(&'a Path),
|
||||||
|
Dependency,
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ pub use crate::lock::export::requirements_txt::RequirementsTxtExport;
|
||||||
use crate::universal_marker::resolve_conflicts;
|
use crate::universal_marker::resolve_conflicts;
|
||||||
use crate::{Installable, LockError, Package};
|
use crate::{Installable, LockError, Package};
|
||||||
|
|
||||||
|
pub mod cyclonedx_json;
|
||||||
mod pylock_toml;
|
mod pylock_toml;
|
||||||
mod requirements_txt;
|
mod requirements_txt;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ use uv_workspace::{Editability, WorkspaceMember};
|
||||||
use crate::fork_strategy::ForkStrategy;
|
use crate::fork_strategy::ForkStrategy;
|
||||||
pub(crate) use crate::lock::export::PylockTomlPackage;
|
pub(crate) use crate::lock::export::PylockTomlPackage;
|
||||||
pub use crate::lock::export::RequirementsTxtExport;
|
pub use crate::lock::export::RequirementsTxtExport;
|
||||||
pub use crate::lock::export::{PylockToml, PylockTomlErrorKind};
|
pub use crate::lock::export::{PylockToml, PylockTomlErrorKind, cyclonedx_json};
|
||||||
pub use crate::lock::installable::Installable;
|
pub use crate::lock::installable::Installable;
|
||||||
pub use crate::lock::map::PackageMap;
|
pub use crate::lock::map::PackageMap;
|
||||||
pub use crate::lock::tree::TreeDisplay;
|
pub use crate::lock::tree::TreeDisplay;
|
||||||
|
|
@ -5963,6 +5963,12 @@ enum LockErrorKind {
|
||||||
#[source]
|
#[source]
|
||||||
err: toml::de::Error,
|
err: toml::de::Error,
|
||||||
},
|
},
|
||||||
|
/// An error that occurs when a workspace member has a non-local source.
|
||||||
|
#[error("Workspace member `{id}` has non-local source", id = id.cyan())]
|
||||||
|
NonLocalWorkspaceMember {
|
||||||
|
/// The ID of the workspace member with an invalid source.
|
||||||
|
id: PackageId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurs when a source string could not be parsed.
|
/// An error that occurs when a source string could not be parsed.
|
||||||
|
|
|
||||||
|
|
@ -228,23 +228,6 @@ impl<'a> OutputWriter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the given arguments to both standard output and the output buffer, if present.
|
|
||||||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
// Write to the buffer.
|
|
||||||
if self.output_file.is_some() {
|
|
||||||
self.buffer.write_fmt(args)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to standard output.
|
|
||||||
if let Some(stdout) = &mut self.stdout {
|
|
||||||
write!(stdout, "{args}")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit the buffer to the output file.
|
/// Commit the buffer to the output file.
|
||||||
async fn commit(self) -> std::io::Result<()> {
|
async fn commit(self) -> std::io::Result<()> {
|
||||||
if let Some(output_file) = self.output_file {
|
if let Some(output_file) = self.output_file {
|
||||||
|
|
@ -263,6 +246,30 @@ impl<'a> OutputWriter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::io::Write for OutputWriter<'_> {
|
||||||
|
/// Write to both standard output and the output buffer, if present.
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
// Write to the buffer.
|
||||||
|
if self.output_file.is_some() {
|
||||||
|
self.buffer.write_all(buf)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to standard output.
|
||||||
|
if let Some(stdout) = &mut self.stdout {
|
||||||
|
stdout.write_all(buf)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
if let Some(stdout) = &mut self.stdout {
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Given a list of names, return a conjunction of the names (e.g., "Alice, Bob, and Charlie").
|
/// Given a list of names, return a conjunction of the names (e.g., "Alice, Bob, and Charlie").
|
||||||
pub(super) fn conjunction(names: Vec<String>) -> String {
|
pub(super) fn conjunction(names: Vec<String>) -> String {
|
||||||
let mut names = names.into_iter();
|
let mut names = names.into_iter();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -15,8 +16,8 @@ use uv_python::downloads::ManagedPythonDownloadList;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
BuildIsolation, BuildOptions, Concurrency, Constraints, ExportFormat, ExtrasSpecification,
|
BuildIsolation, BuildOptions, Concurrency, Constraints, ExtrasSpecification, IndexStrategy,
|
||||||
IndexStrategy, NoBinary, NoBuild, Reinstall, SourceStrategy, Upgrade,
|
NoBinary, NoBuild, PipCompileFormat, Reinstall, SourceStrategy, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_configuration::{KeyringProviderType, TargetTriple};
|
use uv_configuration::{KeyringProviderType, TargetTriple};
|
||||||
use uv_dispatch::{BuildDispatch, SharedState};
|
use uv_dispatch::{BuildDispatch, SharedState};
|
||||||
|
|
@ -75,7 +76,7 @@ pub(crate) async fn pip_compile(
|
||||||
extras: ExtrasSpecification,
|
extras: ExtrasSpecification,
|
||||||
groups: GroupsSpecification,
|
groups: GroupsSpecification,
|
||||||
output_file: Option<&Path>,
|
output_file: Option<&Path>,
|
||||||
format: Option<ExportFormat>,
|
format: Option<PipCompileFormat>,
|
||||||
resolution_mode: ResolutionMode,
|
resolution_mode: ResolutionMode,
|
||||||
prerelease_mode: PrereleaseMode,
|
prerelease_mode: PrereleaseMode,
|
||||||
fork_strategy: ForkStrategy,
|
fork_strategy: ForkStrategy,
|
||||||
|
|
@ -147,16 +148,16 @@ pub(crate) async fn pip_compile(
|
||||||
let format = format.unwrap_or_else(|| {
|
let format = format.unwrap_or_else(|| {
|
||||||
let extension = output_file.and_then(Path::extension);
|
let extension = output_file.and_then(Path::extension);
|
||||||
if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("txt")) {
|
if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("txt")) {
|
||||||
ExportFormat::RequirementsTxt
|
PipCompileFormat::RequirementsTxt
|
||||||
} else if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) {
|
} else if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) {
|
||||||
ExportFormat::PylockToml
|
PipCompileFormat::PylockToml
|
||||||
} else {
|
} else {
|
||||||
ExportFormat::RequirementsTxt
|
PipCompileFormat::RequirementsTxt
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the user is exporting to PEP 751, ensure the filename matches the specification.
|
// If the user is exporting to PEP 751, ensure the filename matches the specification.
|
||||||
if matches!(format, ExportFormat::PylockToml) {
|
if matches!(format, PipCompileFormat::PylockToml) {
|
||||||
if let Some(file_name) = output_file
|
if let Some(file_name) = output_file
|
||||||
.and_then(Path::file_name)
|
.and_then(Path::file_name)
|
||||||
.and_then(OsStr::to_str)
|
.and_then(OsStr::to_str)
|
||||||
|
|
@ -398,7 +399,7 @@ pub(crate) async fn pip_compile(
|
||||||
|
|
||||||
// Generate, but don't enforce hashes for the requirements. PEP 751 _requires_ a hash to be
|
// Generate, but don't enforce hashes for the requirements. PEP 751 _requires_ a hash to be
|
||||||
// present, but otherwise, we omit them by default.
|
// present, but otherwise, we omit them by default.
|
||||||
let hasher = if generate_hashes || matches!(format, ExportFormat::PylockToml) {
|
let hasher = if generate_hashes || matches!(format, PipCompileFormat::PylockToml) {
|
||||||
HashStrategy::Generate(HashGeneration::All)
|
HashStrategy::Generate(HashGeneration::All)
|
||||||
} else {
|
} else {
|
||||||
HashStrategy::None
|
HashStrategy::None
|
||||||
|
|
@ -455,10 +456,10 @@ pub(crate) async fn pip_compile(
|
||||||
let LockedRequirements { preferences, git } =
|
let LockedRequirements { preferences, git } =
|
||||||
if let Some(output_file) = output_file.filter(|output_file| output_file.exists()) {
|
if let Some(output_file) = output_file.filter(|output_file| output_file.exists()) {
|
||||||
match format {
|
match format {
|
||||||
ExportFormat::RequirementsTxt => LockedRequirements::from_preferences(
|
PipCompileFormat::RequirementsTxt => LockedRequirements::from_preferences(
|
||||||
read_requirements_txt(output_file, &upgrade).await?,
|
read_requirements_txt(output_file, &upgrade).await?,
|
||||||
),
|
),
|
||||||
ExportFormat::PylockToml => {
|
PipCompileFormat::PylockToml => {
|
||||||
read_pylock_toml_requirements(output_file, &upgrade).await?
|
read_pylock_toml_requirements(output_file, &upgrade).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -613,7 +614,7 @@ pub(crate) async fn pip_compile(
|
||||||
}
|
}
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
ExportFormat::RequirementsTxt => {
|
PipCompileFormat::RequirementsTxt => {
|
||||||
if include_marker_expression {
|
if include_marker_expression {
|
||||||
if let Some(marker_env) = resolver_env.marker_environment() {
|
if let Some(marker_env) = resolver_env.marker_environment() {
|
||||||
let relevant_markers = resolution.marker_tree(&top_level_index, marker_env)?;
|
let relevant_markers = resolution.marker_tree(&top_level_index, marker_env)?;
|
||||||
|
|
@ -704,7 +705,7 @@ pub(crate) async fn pip_compile(
|
||||||
)
|
)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ExportFormat::PylockToml => {
|
PipCompileFormat::PylockToml => {
|
||||||
if include_marker_expression {
|
if include_marker_expression {
|
||||||
warn_user!(
|
warn_user!(
|
||||||
"The `--emit-marker-expression` option is not supported for `pylock.toml` output"
|
"The `--emit-marker-expression` option is not supported for `pylock.toml` output"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
|
@ -15,7 +16,7 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
||||||
use uv_preview::Preview;
|
use uv_preview::Preview;
|
||||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
||||||
use uv_requirements::is_pylock_toml;
|
use uv_requirements::is_pylock_toml;
|
||||||
use uv_resolver::{PylockToml, RequirementsTxtExport};
|
use uv_resolver::{PylockToml, RequirementsTxtExport, cyclonedx_json};
|
||||||
use uv_scripts::Pep723Script;
|
use uv_scripts::Pep723Script;
|
||||||
use uv_settings::PythonInstallMirrors;
|
use uv_settings::PythonInstallMirrors;
|
||||||
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
|
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
|
||||||
|
|
@ -286,9 +287,6 @@ pub(crate) async fn export(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate that the set of requested extras and development groups are compatible.
|
|
||||||
detect_conflicts(&target, &extras, &groups)?;
|
|
||||||
|
|
||||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||||
target.validate_extras(&extras)?;
|
target.validate_extras(&extras)?;
|
||||||
target.validate_groups(&groups)?;
|
target.validate_groups(&groups)?;
|
||||||
|
|
@ -316,6 +314,11 @@ pub(crate) async fn export(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Skip conflict detection for CycloneDX exports, as SBOMs are meant to document all dependencies including conflicts.
|
||||||
|
if !matches!(format, ExportFormat::CycloneDX1_5) {
|
||||||
|
detect_conflicts(&target, &extras, &groups)?;
|
||||||
|
}
|
||||||
|
|
||||||
// If the user is exporting to PEP 751, ensure the filename matches the specification.
|
// If the user is exporting to PEP 751, ensure the filename matches the specification.
|
||||||
if matches!(format, ExportFormat::PylockToml) {
|
if matches!(format, ExportFormat::PylockToml) {
|
||||||
if let Some(file_name) = output_file
|
if let Some(file_name) = output_file
|
||||||
|
|
@ -376,6 +379,20 @@ pub(crate) async fn export(
|
||||||
}
|
}
|
||||||
write!(writer, "{}", export.to_toml()?)?;
|
write!(writer, "{}", export.to_toml()?)?;
|
||||||
}
|
}
|
||||||
|
ExportFormat::CycloneDX1_5 => {
|
||||||
|
let export = cyclonedx_json::from_lock(
|
||||||
|
&target,
|
||||||
|
&prune,
|
||||||
|
&extras,
|
||||||
|
&groups,
|
||||||
|
include_annotations,
|
||||||
|
&install_options,
|
||||||
|
preview,
|
||||||
|
all_packages,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
export.output_as_json_v1_5(&mut writer)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.commit().await?;
|
writer.commit().await?;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@ use uv_client::Connectivity;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, EnvFile,
|
BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, EnvFile,
|
||||||
ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions,
|
ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions,
|
||||||
KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion,
|
KeyringProviderType, NoBinary, NoBuild, PipCompileFormat, ProjectBuildBackend, Reinstall,
|
||||||
SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
|
RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade,
|
||||||
|
VersionControlSystem,
|
||||||
};
|
};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl,
|
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl,
|
||||||
|
|
@ -2184,7 +2185,7 @@ impl FormatSettings {
|
||||||
/// The resolved settings to use for a `pip compile` invocation.
|
/// The resolved settings to use for a `pip compile` invocation.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct PipCompileSettings {
|
pub(crate) struct PipCompileSettings {
|
||||||
pub(crate) format: Option<ExportFormat>,
|
pub(crate) format: Option<PipCompileFormat>,
|
||||||
pub(crate) src_file: Vec<PathBuf>,
|
pub(crate) src_file: Vec<PathBuf>,
|
||||||
pub(crate) constraints: Vec<PathBuf>,
|
pub(crate) constraints: Vec<PathBuf>,
|
||||||
pub(crate) overrides: Vec<PathBuf>,
|
pub(crate) overrides: Vec<PathBuf>,
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,27 @@ impl TestContext {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds filters for non-deterministic `CycloneDX` data
|
||||||
|
pub fn with_cyclonedx_filters(mut self) -> Self {
|
||||||
|
self.filters.push((
|
||||||
|
r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(),
|
||||||
|
"[SERIAL_NUMBER]".to_string(),
|
||||||
|
));
|
||||||
|
self.filters.push((
|
||||||
|
r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
|
||||||
|
.to_string(),
|
||||||
|
r#""timestamp": "[TIMESTAMP]""#.to_string(),
|
||||||
|
));
|
||||||
|
self.filters.push((
|
||||||
|
r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
|
||||||
|
.to_string(),
|
||||||
|
r#""name": "uv",
|
||||||
|
"version": "[VERSION]""#
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a filter that collapses duplicate whitespace.
|
/// Add a filter that collapses duplicate whitespace.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_collapsed_whitespace(mut self) -> Self {
|
pub fn with_collapsed_whitespace(mut self) -> Self {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7831,7 +7831,7 @@ fn preview_features() {
|
||||||
show_settings: true,
|
show_settings: true,
|
||||||
preview: Preview {
|
preview: Preview {
|
||||||
flags: PreviewFeatures(
|
flags: PreviewFeatures(
|
||||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST,
|
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
python_preference: Managed,
|
python_preference: Managed,
|
||||||
|
|
@ -8059,7 +8059,7 @@ fn preview_features() {
|
||||||
show_settings: true,
|
show_settings: true,
|
||||||
preview: Preview {
|
preview: Preview {
|
||||||
flags: PreviewFeatures(
|
flags: PreviewFeatures(
|
||||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST,
|
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
python_preference: Managed,
|
python_preference: Managed,
|
||||||
|
|
|
||||||
|
|
@ -186,12 +186,17 @@ environment.
|
||||||
|
|
||||||
## Exporting the lockfile
|
## Exporting the lockfile
|
||||||
|
|
||||||
If you need to integrate uv with other tools or workflows, you can export `uv.lock` to the
|
If you need to integrate uv with other tools or workflows, you can export `uv.lock` to different
|
||||||
`requirements.txt` format with `uv export --format requirements-txt`. The generated
|
formats including `requirements.txt`, `pylock.toml` (PEP 751), and CycloneDX SBOM.
|
||||||
`requirements.txt` file can then be installed via `uv pip install`, or with other tools like `pip`.
|
|
||||||
|
|
||||||
In general, we recommend against using both a `uv.lock` and a `requirements.txt` file. If you find
|
```console
|
||||||
yourself exporting a `uv.lock` file, consider opening an issue to discuss your use case.
|
$ uv export --format requirements.txt
|
||||||
|
$ uv export --format pylock.toml
|
||||||
|
$ uv export --format cyclonedx1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [export guide](../../guides/export.md) for comprehensive documentation on all export formats
|
||||||
|
and their use cases.
|
||||||
|
|
||||||
## Partial installations
|
## Partial installations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
---
|
||||||
|
title: Exporting a lockfile
|
||||||
|
description: Exporting a lockfile to different formats
|
||||||
|
---
|
||||||
|
|
||||||
|
# Exporting a lockfile
|
||||||
|
|
||||||
|
uv can export a lockfile to different formats for integration with other tools and workflows. The
|
||||||
|
`uv export` command supports multiple output formats, each suited to different use cases.
|
||||||
|
|
||||||
|
For more details on lockfiles and how they're created, see the
|
||||||
|
[project layout](../concepts/projects/layout.md) and
|
||||||
|
[locking and syncing](../concepts/projects/sync.md) documentation.
|
||||||
|
|
||||||
|
## Overview of export formats
|
||||||
|
|
||||||
|
uv supports three export formats:
|
||||||
|
|
||||||
|
- `requirements.txt`: The traditional pip-compatible
|
||||||
|
[requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/).
|
||||||
|
- `pylock.toml`: The standardized Python lockfile format defined in
|
||||||
|
[PEP 751](https://peps.python.org/pep-0751/).
|
||||||
|
- `CycloneDX`: An industry-standard [Software Bill of Materials (SBOM)](https://cyclonedx.org/)
|
||||||
|
format.
|
||||||
|
|
||||||
|
The format can be specified with the `--format` flag:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ uv export --format requirements.txt
|
||||||
|
$ uv export --format pylock.toml
|
||||||
|
$ uv export --format cyclonedx1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
By default, `uv export` prints to stdout. Use `--output-file` to write to a file for any format:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ uv export --format requirements.txt --output-file requirements.txt
|
||||||
|
$ uv export --format pylock.toml --output-file pylock.toml
|
||||||
|
$ uv export --format cyclonedx1.5 --output-file sbom.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## `requirements.txt` format
|
||||||
|
|
||||||
|
The `requirements.txt` format is the most widely supported format for Python dependencies. It can be
|
||||||
|
used with `pip` and other Python package managers.
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ uv export --format requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated `requirements.txt` file can then be installed via `uv pip install`, or with other
|
||||||
|
tools like `pip`.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
In general, we recommend against using both a `uv.lock` and a `requirements.txt` file. The
|
||||||
|
`uv.lock` format is more powerful and includes features that cannot be expressed in
|
||||||
|
`requirements.txt`. If you find yourself exporting a `uv.lock` file, consider opening an issue
|
||||||
|
to discuss your use case.
|
||||||
|
|
||||||
|
## `pylock.toml` format
|
||||||
|
|
||||||
|
[PEP 751](https://peps.python.org/pep-0751/) defines a TOML-based lockfile format for Python
|
||||||
|
dependencies. uv can export your project's dependency lockfile to this format.
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ uv export --format pylock.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## CycloneDX SBOM format
|
||||||
|
|
||||||
|
uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX
|
||||||
|
format. SBOMs provide a comprehensive inventory of all software components in your application,
|
||||||
|
which is useful for security auditing, compliance, and supply chain transparency.
|
||||||
|
|
||||||
|
!!! important
|
||||||
|
|
||||||
|
Support for exporting to CycloneDX is in [preview](../concepts/preview.md), and may change in any future release.
|
||||||
|
|
||||||
|
### What is CycloneDX?
|
||||||
|
|
||||||
|
[CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of
|
||||||
|
Materials. CycloneDX is machine readable and widely supported by security scanning tools,
|
||||||
|
vulnerability databases, and Software Composition Analysis (SCA) platforms.
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
To export your project's lockfile as a CycloneDX SBOM:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ uv export --format cyclonedx1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate a JSON-encoded CycloneDX v1.5 document containing your project and all of its
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
### SBOM Structure
|
||||||
|
|
||||||
|
The generated SBOM follows the
|
||||||
|
[CycloneDX specification](https://cyclonedx.org/specification/overview/). uv also includes the
|
||||||
|
following custom properties on components:
|
||||||
|
|
||||||
|
- `uv:package:marker`: Environment markers (e.g., `python_version >= "3.8"`)
|
||||||
|
- `uv:workspace:path`: Relative path for workspace members
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
To learn more about lockfiles and exporting, see the
|
||||||
|
[locking and syncing](../concepts/projects/sync.md) documentation and the
|
||||||
|
[command reference](../reference/cli.md#uv-export).
|
||||||
|
|
||||||
|
Or, read on to learn how to [build and publish your project to a package index](./package.md).
|
||||||
|
|
@ -272,4 +272,4 @@ To learn more about working on projects with uv, see the
|
||||||
[projects concept](../concepts/projects/index.md) page and the
|
[projects concept](../concepts/projects/index.md) page and the
|
||||||
[command reference](../reference/cli.md#uv).
|
[command reference](../reference/cli.md#uv).
|
||||||
|
|
||||||
Or, read on to learn how to [build and publish your project to a package index](./package.md).
|
Or, read on to learn how to [export a uv lockfile to different formats](./export.md).
|
||||||
|
|
|
||||||
|
|
@ -1855,12 +1855,13 @@ uv export [OPTIONS]
|
||||||
<li><code>fewest</code>: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms</li>
|
<li><code>fewest</code>: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms</li>
|
||||||
<li><code>requires-python</code>: Optimize for selecting latest supported version of each package, for each supported Python version</li>
|
<li><code>requires-python</code>: Optimize for selecting latest supported version of each package, for each supported Python version</li>
|
||||||
</ul></dd><dt id="uv-export--format"><a href="#uv-export--format"><code>--format</code></a> <i>format</i></dt><dd><p>The format to which <code>uv.lock</code> should be exported.</p>
|
</ul></dd><dt id="uv-export--format"><a href="#uv-export--format"><code>--format</code></a> <i>format</i></dt><dd><p>The format to which <code>uv.lock</code> should be exported.</p>
|
||||||
<p>Supports both <code>requirements.txt</code> and <code>pylock.toml</code> (PEP 751) output formats.</p>
|
<p>Supports <code>requirements.txt</code>, <code>pylock.toml</code> (PEP 751) and CycloneDX v1.5 JSON output formats.</p>
|
||||||
<p>uv will infer the output format from the file extension of the output file, if provided. Otherwise, defaults to <code>requirements.txt</code>.</p>
|
<p>uv will infer the output format from the file extension of the output file, if provided. Otherwise, defaults to <code>requirements.txt</code>.</p>
|
||||||
<p>Possible values:</p>
|
<p>Possible values:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>requirements.txt</code>: Export in <code>requirements.txt</code> format</li>
|
<li><code>requirements.txt</code>: Export in <code>requirements.txt</code> format</li>
|
||||||
<li><code>pylock.toml</code>: Export in <code>pylock.toml</code> format</li>
|
<li><code>pylock.toml</code>: Export in <code>pylock.toml</code> format</li>
|
||||||
|
<li><code>cyclonedx1.5</code>: Export in <code>CycloneDX</code> v1.5 JSON format</li>
|
||||||
</ul></dd><dt id="uv-export--frozen"><a href="#uv-export--frozen"><code>--frozen</code></a></dt><dd><p>Do not update the <code>uv.lock</code> before exporting.</p>
|
</ul></dd><dt id="uv-export--frozen"><a href="#uv-export--frozen"><code>--frozen</code></a></dt><dd><p>Do not update the <code>uv.lock</code> before exporting.</p>
|
||||||
<p>If a <code>uv.lock</code> does not exist, uv will exit with an error.</p>
|
<p>If a <code>uv.lock</code> does not exist, uv will exit with an error.</p>
|
||||||
<p>May also be set with the <code>UV_FROZEN</code> environment variable.</p></dd><dt id="uv-export--group"><a href="#uv-export--group"><code>--group</code></a> <i>group</i></dt><dd><p>Include dependencies from the specified dependency group.</p>
|
<p>May also be set with the <code>UV_FROZEN</code> environment variable.</p></dd><dt id="uv-export--group"><a href="#uv-export--group"><code>--group</code></a> <i>group</i></dt><dd><p>Include dependencies from the specified dependency group.</p>
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ plugins:
|
||||||
- guides/scripts.md
|
- guides/scripts.md
|
||||||
- guides/tools.md
|
- guides/tools.md
|
||||||
- guides/projects.md
|
- guides/projects.md
|
||||||
|
- guides/export.md
|
||||||
- guides/package.md
|
- guides/package.md
|
||||||
Integrations:
|
Integrations:
|
||||||
- guides/integration/docker.md
|
- guides/integration/docker.md
|
||||||
|
|
@ -177,6 +178,7 @@ nav:
|
||||||
- Running scripts: guides/scripts.md
|
- Running scripts: guides/scripts.md
|
||||||
- Using tools: guides/tools.md
|
- Using tools: guides/tools.md
|
||||||
- Working on projects: guides/projects.md
|
- Working on projects: guides/projects.md
|
||||||
|
- Exporting lockfiles: guides/export.md
|
||||||
- Publishing packages: guides/package.md
|
- Publishing packages: guides/package.md
|
||||||
- Migration:
|
- Migration:
|
||||||
- guides/migration/index.md
|
- guides/migration/index.md
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue