vendoring 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vendoring/__init__.py +6 -0
- vendoring/__main__.py +4 -0
- vendoring/cli.py +94 -0
- vendoring/configuration.py +178 -0
- vendoring/errors.py +10 -0
- vendoring/interactive.py +197 -0
- vendoring/sbom.py +53 -0
- vendoring/sync.py +19 -0
- vendoring/tasks/__init__.py +0 -0
- vendoring/tasks/cleanup.py +32 -0
- vendoring/tasks/license.py +166 -0
- vendoring/tasks/stubs.py +52 -0
- vendoring/tasks/update.py +89 -0
- vendoring/tasks/vendor.py +182 -0
- vendoring/ui.py +115 -0
- vendoring/utils.py +62 -0
- vendoring-1.3.0.dist-info/METADATA +53 -0
- vendoring-1.3.0.dist-info/RECORD +21 -0
- vendoring-1.3.0.dist-info/WHEEL +4 -0
- vendoring-1.3.0.dist-info/entry_points.txt +3 -0
- vendoring-1.3.0.dist-info/licenses/LICENSE +21 -0
vendoring/__init__.py
ADDED
vendoring/__main__.py
ADDED
vendoring/cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable, List, NamedTuple, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from vendoring.configuration import load_configuration
|
|
8
|
+
from vendoring.errors import VendoringError
|
|
9
|
+
from vendoring.interactive import interactive_updates
|
|
10
|
+
from vendoring.sync import run_sync
|
|
11
|
+
from vendoring.tasks.update import update_requirements
|
|
12
|
+
from vendoring.ui import UI
|
|
13
|
+
|
|
14
|
+
_EntryPoint = Callable[..., None]
|
|
15
|
+
_Param = Callable[[_EntryPoint], _EntryPoint]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Template(NamedTuple):
|
|
19
|
+
# Arguments
|
|
20
|
+
package: _Param
|
|
21
|
+
# Options
|
|
22
|
+
verbose: _Param
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
template = _Template(
|
|
26
|
+
package=click.argument("package", default=None, required=False, type=str),
|
|
27
|
+
verbose=click.option("-v", "--verbose", is_flag=True),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def main() -> None:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@main.command()
|
|
37
|
+
@template.verbose
|
|
38
|
+
def sync(verbose: bool) -> None:
|
|
39
|
+
"""Vendor libraries as described in lockfile"""
|
|
40
|
+
UI.verbose = verbose
|
|
41
|
+
project_path = Path()
|
|
42
|
+
|
|
43
|
+
print(f"Working in {project_path}")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with UI.task("Load configuration"):
|
|
47
|
+
config = load_configuration(project_path)
|
|
48
|
+
run_sync(config)
|
|
49
|
+
except VendoringError as e:
|
|
50
|
+
UI.show_error(e)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@main.command()
|
|
55
|
+
@template.verbose
|
|
56
|
+
@template.package
|
|
57
|
+
def update(verbose: bool, package: Optional[str]) -> None:
|
|
58
|
+
"""Update a single package version"""
|
|
59
|
+
UI.verbose = verbose
|
|
60
|
+
project_path = Path()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with UI.task("Load configuration"):
|
|
64
|
+
config = load_configuration(project_path)
|
|
65
|
+
with UI.task("Updating requirements"):
|
|
66
|
+
update_requirements(config, package)
|
|
67
|
+
except VendoringError as e:
|
|
68
|
+
UI.show_error(e)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@main.command("update-interactive")
|
|
73
|
+
@click.option("--skip", help="Skip named packages", multiple=True)
|
|
74
|
+
@click.option("--only", help="Only update named packages", multiple=True)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--from-start",
|
|
77
|
+
help="Start from the beginning, ignoring existing markers",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
)
|
|
80
|
+
@template.verbose
|
|
81
|
+
def update_interactive(
|
|
82
|
+
verbose: bool, skip: List[str], only: List[str], from_start: bool
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Update all package versions, interactively"""
|
|
85
|
+
UI.verbose = verbose
|
|
86
|
+
project_path = Path()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with UI.task("Load configuration"):
|
|
90
|
+
config = load_configuration(project_path)
|
|
91
|
+
interactive_updates(config, skip=skip, only=only, from_start=from_start)
|
|
92
|
+
except VendoringError as e:
|
|
93
|
+
UI.show_error(e)
|
|
94
|
+
sys.exit(1)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Loads configuration from pyproject.toml"""
|
|
2
|
+
|
|
3
|
+
# mypy: allow-any-generics, allow-any-explicit
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from jsonschema import ValidationError, validate
|
|
10
|
+
from tomli import TOMLDecodeError
|
|
11
|
+
from tomli import loads as parse_toml
|
|
12
|
+
|
|
13
|
+
from vendoring.errors import ConfigurationError
|
|
14
|
+
from vendoring.ui import UI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Configuration:
|
|
19
|
+
# Base directory for all of the operation of this project
|
|
20
|
+
base_directory: Path
|
|
21
|
+
|
|
22
|
+
# Location to unpack into
|
|
23
|
+
destination: Path
|
|
24
|
+
# Final namespace to rewrite imports to originate from
|
|
25
|
+
namespace: str
|
|
26
|
+
# Path to a pip-style requirement files
|
|
27
|
+
requirements: Path
|
|
28
|
+
# Filenames to ignore in target directory
|
|
29
|
+
protected_files: List[str]
|
|
30
|
+
# Location to ``.patch` files to apply after vendoring
|
|
31
|
+
patches_dir: Optional[Path]
|
|
32
|
+
|
|
33
|
+
# Additional substitutions, done in addition to import rewriting
|
|
34
|
+
substitute: List[Dict[str, str]]
|
|
35
|
+
# Drop
|
|
36
|
+
drop_paths: List[str]
|
|
37
|
+
|
|
38
|
+
# Fallbacks for licenses that can't be found
|
|
39
|
+
license_fallback_urls: Dict[str, str]
|
|
40
|
+
# Alternate directory name, when distribution name differs from the package name
|
|
41
|
+
license_directories: Dict[str, str]
|
|
42
|
+
|
|
43
|
+
# Overrides for which stub files are generated
|
|
44
|
+
typing_stubs: Dict[str, List[str]]
|
|
45
|
+
|
|
46
|
+
# SBOM file
|
|
47
|
+
sbom_file: Optional[Path]
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def load_from_dict(
|
|
51
|
+
cls, dictionary: Dict[str, Any], *, location: Path
|
|
52
|
+
) -> "Configuration":
|
|
53
|
+
"""Constructs a Configuration object from dictionary.
|
|
54
|
+
|
|
55
|
+
It also performs validation of the values in `dictionary`,
|
|
56
|
+
expecting paths to be within `location`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
schema = {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": False,
|
|
62
|
+
"required": ["destination", "namespace", "requirements"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"destination": {"type": "string"},
|
|
65
|
+
"namespace": {"type": "string"},
|
|
66
|
+
"requirements": {"type": "string"},
|
|
67
|
+
"protected-files": {"type": "array", "items": {"type": "string"}},
|
|
68
|
+
"patches-dir": {"type": "string"},
|
|
69
|
+
"sbom-file": {"type": "string"},
|
|
70
|
+
"transformations": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": False,
|
|
73
|
+
"properties": {
|
|
74
|
+
"substitute": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": False,
|
|
79
|
+
"required": ["match", "replace"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"match": {"type": "string"},
|
|
82
|
+
"replace": {"type": "string"},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
"drop": {"type": "array", "items": {"type": "string"}},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"license": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": False,
|
|
92
|
+
"properties": {
|
|
93
|
+
"directories": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"patternProperties": {"^.*$": {"type": "string"}},
|
|
96
|
+
},
|
|
97
|
+
"fallback-urls": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"patternProperties": {"^.*$": {"type": "string"}},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
"typing-stubs": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"patternProperties": {
|
|
106
|
+
"^.*$": {"type": "array", "items": {"type": "string"}},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
validate(dictionary, schema)
|
|
114
|
+
except ValidationError as e:
|
|
115
|
+
raise ConfigurationError(str(e))
|
|
116
|
+
|
|
117
|
+
def path_or_none(key: str) -> Optional[Path]:
|
|
118
|
+
if key in dictionary:
|
|
119
|
+
return Path(dictionary[key])
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
return Configuration(
|
|
123
|
+
base_directory=location,
|
|
124
|
+
destination=Path(dictionary["destination"]),
|
|
125
|
+
namespace=dictionary["namespace"],
|
|
126
|
+
requirements=Path(dictionary["requirements"]),
|
|
127
|
+
protected_files=dictionary.get("protected-files", []),
|
|
128
|
+
patches_dir=path_or_none("patches-dir"),
|
|
129
|
+
sbom_file=path_or_none("sbom-file"),
|
|
130
|
+
substitute=dictionary.get("transformations", {}).get("substitute", {}),
|
|
131
|
+
drop_paths=dictionary.get("transformations", {}).get("drop", []),
|
|
132
|
+
license_fallback_urls=dictionary.get("license", {}).get(
|
|
133
|
+
"fallback-urls", {}
|
|
134
|
+
),
|
|
135
|
+
license_directories=dictionary.get("license", {}).get("directories", {}),
|
|
136
|
+
typing_stubs=dictionary.get("typing-stubs", {}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_configuration(directory: Path) -> Configuration:
|
|
141
|
+
# Read the contents of the file.
|
|
142
|
+
file = directory / "pyproject.toml"
|
|
143
|
+
UI.log(f"Will attempt to load {file}.")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
file_contents = file.read_text(encoding="utf8")
|
|
147
|
+
except IOError as read_error:
|
|
148
|
+
raise ConfigurationError("Could not read pyproject.toml.") from read_error
|
|
149
|
+
else:
|
|
150
|
+
UI.log("Read configuration file.")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
parsed_contents = parse_toml(file_contents)
|
|
154
|
+
except TOMLDecodeError as toml_error:
|
|
155
|
+
raise ConfigurationError("Could not parse pyproject.toml.") from toml_error
|
|
156
|
+
else:
|
|
157
|
+
UI.log("Parsed configuration file.")
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
"tool" not in parsed_contents
|
|
161
|
+
or not isinstance(parsed_contents["tool"], dict)
|
|
162
|
+
or "vendoring" not in parsed_contents["tool"]
|
|
163
|
+
or not isinstance(parsed_contents["tool"]["vendoring"], dict)
|
|
164
|
+
):
|
|
165
|
+
raise ConfigurationError("Can not load `tool.vendoring` from pyproject.toml")
|
|
166
|
+
|
|
167
|
+
tool_config = parsed_contents["tool"]["vendoring"]
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
retval = Configuration.load_from_dict(tool_config, location=directory)
|
|
171
|
+
except ConfigurationError as e:
|
|
172
|
+
raise ConfigurationError(
|
|
173
|
+
"Could not load values from [tool.vendoring] in pyproject.toml.\n"
|
|
174
|
+
f" REASON: {e}"
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
UI.log("Validated configuration.")
|
|
178
|
+
return retval
|
vendoring/errors.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class VendoringError(Exception):
|
|
2
|
+
"""Errors originating from this package."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(VendoringError):
|
|
6
|
+
"""Errors related to configuration handling."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RequirementsError(VendoringError):
|
|
10
|
+
"""Errors related to requirements.txt handling."""
|
vendoring/interactive.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Interactive mode, for updating all packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import click as _click
|
|
9
|
+
from packaging.version import Version
|
|
10
|
+
|
|
11
|
+
from vendoring.configuration import Configuration
|
|
12
|
+
from vendoring.errors import VendoringError
|
|
13
|
+
from vendoring.sync import run_sync
|
|
14
|
+
from vendoring.tasks.update import (
|
|
15
|
+
PinnedPackageInfo,
|
|
16
|
+
determine_latest_release,
|
|
17
|
+
parse_pinned_packages,
|
|
18
|
+
)
|
|
19
|
+
from vendoring.ui import UI
|
|
20
|
+
|
|
21
|
+
Action = Literal["skip", "incomplete", "done"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InteractionState:
|
|
25
|
+
"""Manages the state of the interactive session.
|
|
26
|
+
|
|
27
|
+
This handles the following:
|
|
28
|
+
- Reading the requirements file
|
|
29
|
+
- Writing the requirements file
|
|
30
|
+
- Reading the "state" file
|
|
31
|
+
- Keeping track of the current package, in memory and in the "state" file
|
|
32
|
+
- Writing the "state" file
|
|
33
|
+
- Cleaning up the "state" file
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: Configuration) -> None:
|
|
37
|
+
self._requirements = config.requirements
|
|
38
|
+
self._state_file = (
|
|
39
|
+
config.base_directory
|
|
40
|
+
/ ".vendoring_cache"
|
|
41
|
+
/ "do-not-commit.interactive.current-package"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
packages = parse_pinned_packages(self._requirements)
|
|
45
|
+
self._packages = {p.name: p for p in packages}
|
|
46
|
+
self._package_order = [p.name for p in packages]
|
|
47
|
+
|
|
48
|
+
self._current_package: str | None = None
|
|
49
|
+
if self._state_file.exists():
|
|
50
|
+
self._current_package = self._state_file.read_text()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def packages(self) -> tuple[tuple[str, str], ...]:
|
|
54
|
+
"""Return the list of packages."""
|
|
55
|
+
return tuple((p.name, p.version) for p in self._packages.values())
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def resuming_from(self) -> PinnedPackageInfo | None:
|
|
59
|
+
"""Return the package we are resuming from."""
|
|
60
|
+
if self._current_package is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
if self._current_package not in self._packages:
|
|
64
|
+
raise VendoringError(
|
|
65
|
+
"The package to 'resume from' is not in requirements file\n"
|
|
66
|
+
f"package_name: {self._current_package}\n"
|
|
67
|
+
f"packages: {list(self._packages)}\n"
|
|
68
|
+
"Please run with `--from-start` to reset the state."
|
|
69
|
+
)
|
|
70
|
+
return self._packages[self._current_package]
|
|
71
|
+
|
|
72
|
+
def get_info(self, package: list[str]) -> list[PinnedPackageInfo]:
|
|
73
|
+
return [self._packages[p] for p in package]
|
|
74
|
+
|
|
75
|
+
def update(self, package_name: str, version: str) -> None:
|
|
76
|
+
"""Update the state of the session."""
|
|
77
|
+
self._packages[package_name].version = version
|
|
78
|
+
with self._requirements.open("w", encoding="utf-8") as f:
|
|
79
|
+
f.writelines(f"{self._packages[p]}\n" for p in self._package_order)
|
|
80
|
+
|
|
81
|
+
self._current_package = package_name
|
|
82
|
+
if not self._state_file.parent.exists():
|
|
83
|
+
self._state_file.parent.mkdir()
|
|
84
|
+
self._state_file.write_text(package_name)
|
|
85
|
+
|
|
86
|
+
def cleanup(self) -> None:
|
|
87
|
+
if not self._state_file.exists():
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self._state_file.unlink()
|
|
91
|
+
self._state_file.parent.rmdir()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def git(*args: str) -> None:
|
|
95
|
+
"""Run a git command."""
|
|
96
|
+
cmd = ["git", *args]
|
|
97
|
+
|
|
98
|
+
UI.log(" ".join(map(str, cmd)))
|
|
99
|
+
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def do_one_update(config: Configuration, *, name: str, version: str) -> None:
|
|
103
|
+
"""Perform the update of a single package."""
|
|
104
|
+
run_sync(config)
|
|
105
|
+
|
|
106
|
+
# Determine the correct message
|
|
107
|
+
message = f"Upgrade {name} to {version}"
|
|
108
|
+
# Write our news fragment
|
|
109
|
+
news_file = config.base_directory / "news" / (name + ".vendor.rst")
|
|
110
|
+
news_file.write_text(message + "\n") # "\n" appeases end-of-line-fixer
|
|
111
|
+
|
|
112
|
+
# Commit the changes
|
|
113
|
+
git("add", str(news_file))
|
|
114
|
+
git("commit", "-m", message)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def determine_packages(
|
|
118
|
+
packages: tuple[tuple[str, str], ...],
|
|
119
|
+
*,
|
|
120
|
+
resuming_from: PinnedPackageInfo | None,
|
|
121
|
+
skip: list[str],
|
|
122
|
+
only: list[str],
|
|
123
|
+
) -> list[str]:
|
|
124
|
+
"""Determine the packages to update."""
|
|
125
|
+
if resuming_from is not None:
|
|
126
|
+
if resuming_from.name in skip:
|
|
127
|
+
raise VendoringError(
|
|
128
|
+
f"Cannot resume from {resuming_from!r} as it is in the skip list"
|
|
129
|
+
)
|
|
130
|
+
if only and resuming_from.name not in only:
|
|
131
|
+
raise VendoringError(
|
|
132
|
+
f"Cannot resume from {resuming_from!r} as it is not in the only list"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if only:
|
|
136
|
+
known = {package[0] for package in packages}
|
|
137
|
+
for name in only:
|
|
138
|
+
if name not in known:
|
|
139
|
+
raise VendoringError(
|
|
140
|
+
f"Package {name!r} is not in the requirements file"
|
|
141
|
+
)
|
|
142
|
+
return only
|
|
143
|
+
|
|
144
|
+
if skip:
|
|
145
|
+
return [package[0] for package in packages if package[0] not in skip]
|
|
146
|
+
|
|
147
|
+
return [package[0] for package in packages]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def interactive_updates(
|
|
151
|
+
config: Configuration, *, skip: list[str], only: list[str], from_start: bool
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Interactive mode, for updating all packages."""
|
|
154
|
+
with UI.task("Read incoming requirements"):
|
|
155
|
+
state = InteractionState(config)
|
|
156
|
+
|
|
157
|
+
if from_start:
|
|
158
|
+
state.cleanup()
|
|
159
|
+
|
|
160
|
+
resuming_from = state.resuming_from
|
|
161
|
+
package_names = determine_packages(
|
|
162
|
+
state.packages, resuming_from=resuming_from, skip=skip, only=only
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _present(package_info: PinnedPackageInfo, *, prefix: str | None = None) -> None:
|
|
166
|
+
real_prefix = f"{prefix} " if prefix else ""
|
|
167
|
+
UI.log(
|
|
168
|
+
f"{real_prefix}"
|
|
169
|
+
f"{_click.style(package_info.name, fg='green')} "
|
|
170
|
+
f"{_click.style('==', fg='blue')} "
|
|
171
|
+
f"{_click.style(package_info.version, fg='magenta')}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if resuming_from is not None:
|
|
175
|
+
_present(resuming_from, prefix="Resuming from")
|
|
176
|
+
with UI.indent():
|
|
177
|
+
do_one_update(
|
|
178
|
+
config, name=resuming_from.name, version=resuming_from.version
|
|
179
|
+
)
|
|
180
|
+
UI.log(f"Processing remaining {len(package_names)} package(s)")
|
|
181
|
+
else:
|
|
182
|
+
UI.log(f"Processing {len(package_names)} package(s)")
|
|
183
|
+
|
|
184
|
+
with UI.indent():
|
|
185
|
+
for package_info in state.get_info(package_names):
|
|
186
|
+
_present(package_info)
|
|
187
|
+
with UI.indent():
|
|
188
|
+
latest_version = determine_latest_release(package_info.name)
|
|
189
|
+
if Version(latest_version) <= Version(package_info.version):
|
|
190
|
+
UI.log("Already up-to-date")
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
state.update(package_info.name, latest_version)
|
|
194
|
+
do_one_update(config, name=package_info.name, version=latest_version)
|
|
195
|
+
|
|
196
|
+
UI.log("All done, removing marker file")
|
|
197
|
+
state.cleanup()
|
vendoring/sbom.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Code for generating a Software Bill-of-Materials (SBOM)
|
|
2
|
+
from vendored libraries.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from vendoring import __version__ as _vendoring_version
|
|
11
|
+
from vendoring.tasks.update import parse_pinned_packages as _parse_pinned_packages
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_sbom_file(namespace: str, requirements: Path, sbom_file: Path) -> None:
|
|
15
|
+
# The top-most name in the module namespace is the
|
|
16
|
+
# most likely to be a recognizable name.
|
|
17
|
+
top_level = namespace.split(".", 1)[0]
|
|
18
|
+
top_level_bom_ref = f"bom-ref:{top_level}"
|
|
19
|
+
components: List[Any] = [
|
|
20
|
+
{"bom-ref": top_level_bom_ref, "name": top_level, "type": "library"}
|
|
21
|
+
]
|
|
22
|
+
dependencies: List[Any] = [{"ref": top_level_bom_ref, "dependsOn": []}]
|
|
23
|
+
sbom = {
|
|
24
|
+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
|
25
|
+
"bomFormat": "CycloneDX",
|
|
26
|
+
"specVersion": "1.4",
|
|
27
|
+
"version": 1,
|
|
28
|
+
"metadata": {
|
|
29
|
+
"tools": [{"name": "vendoring", "version": _vendoring_version}],
|
|
30
|
+
"component": components[0],
|
|
31
|
+
},
|
|
32
|
+
"components": components,
|
|
33
|
+
"dependencies": dependencies,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pkgs = sorted(
|
|
37
|
+
_parse_pinned_packages(requirements), key=lambda item: (item.name, item.version)
|
|
38
|
+
)
|
|
39
|
+
for pkg in pkgs:
|
|
40
|
+
purl = f"pkg:pypi/{quote(pkg.name, safe='')}@{quote(pkg.version, safe='')}"
|
|
41
|
+
components.append(
|
|
42
|
+
{
|
|
43
|
+
"name": pkg.name,
|
|
44
|
+
"version": pkg.version,
|
|
45
|
+
"purl": purl,
|
|
46
|
+
"type": "library",
|
|
47
|
+
"bom-ref": purl,
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
dependencies[0]["dependsOn"].append(purl)
|
|
51
|
+
dependencies.append({"ref": purl})
|
|
52
|
+
|
|
53
|
+
sbom_file.write_text(json.dumps(sbom, indent=2, sort_keys=True))
|
vendoring/sync.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Core logic of the sync task."""
|
|
2
|
+
|
|
3
|
+
from vendoring.configuration import Configuration
|
|
4
|
+
from vendoring.tasks.cleanup import cleanup_existing_vendored
|
|
5
|
+
from vendoring.tasks.license import fetch_licenses
|
|
6
|
+
from vendoring.tasks.stubs import generate_stubs
|
|
7
|
+
from vendoring.tasks.vendor import vendor_libraries
|
|
8
|
+
from vendoring.ui import UI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_sync(config: Configuration) -> None:
|
|
12
|
+
with UI.task("Clean existing libraries"):
|
|
13
|
+
cleanup_existing_vendored(config)
|
|
14
|
+
with UI.task("Add vendored libraries"):
|
|
15
|
+
libraries = vendor_libraries(config)
|
|
16
|
+
with UI.task("Fetch licenses"):
|
|
17
|
+
fetch_licenses(config)
|
|
18
|
+
with UI.task("Generate static-typing stubs"):
|
|
19
|
+
generate_stubs(config, libraries)
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Logic for cleaning up already vendored files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable, List
|
|
5
|
+
|
|
6
|
+
from vendoring.configuration import Configuration
|
|
7
|
+
from vendoring.utils import remove_all
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def determine_items_to_remove(
|
|
11
|
+
destination: Path, *, files_to_skip: List[str]
|
|
12
|
+
) -> Iterable[Path]:
|
|
13
|
+
if not destination.exists():
|
|
14
|
+
# Folder does not exist, nothing to cleanup.
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
for item in destination.iterdir():
|
|
18
|
+
if item.is_dir():
|
|
19
|
+
# Directory
|
|
20
|
+
yield item
|
|
21
|
+
elif item.name not in files_to_skip:
|
|
22
|
+
# File, not in files_to_skip
|
|
23
|
+
yield item
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cleanup_existing_vendored(config: Configuration) -> None:
|
|
27
|
+
"""Cleans up existing vendored files in `destination` directory."""
|
|
28
|
+
destination = config.destination
|
|
29
|
+
items = determine_items_to_remove(destination, files_to_skip=config.protected_files)
|
|
30
|
+
|
|
31
|
+
# TODO: log how many items were removed.
|
|
32
|
+
remove_all(items, protected=[config.requirements])
|