Checks / tests (ubuntu-latest, 3.11) (push) Canceled after 0s
Checks / tests (ubuntu-latest, 3.14) (push) Canceled after 0s
Checks / tests (windows-latest, 3.11) (push) Canceled after 0s
Checks / tests (windows-latest, 3.14) (push) Canceled after 0s
Checks / package (push) Canceled after 0s
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""Copy an explicit source allowlist into a new repository directory.
|
|
|
|
This workspace also holds firmware research. Exporting from an allowlist,
|
|
rather than recursively copying the workspace and subtracting known inputs,
|
|
prevents newly added vendor artifacts from silently entering a release.
|
|
The destination must be empty; existing content is never replaced.
|
|
"""
|
|
|
|
import argparse
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
ROOT_FILES = (
|
|
"README.md",
|
|
"CONTRIBUTING.md",
|
|
"LICENSE",
|
|
"LICENSE.txt",
|
|
"pyproject.toml",
|
|
"uv.lock",
|
|
"MANIFEST.in",
|
|
"requirements.txt",
|
|
"requirements-diagnostics.txt",
|
|
".gitignore",
|
|
)
|
|
TREES = {
|
|
"controller": {".py"},
|
|
"tests": {".py"},
|
|
"docs": {".md"},
|
|
"scripts": {".py"},
|
|
".github": {".yml", ".yaml"},
|
|
}
|
|
|
|
|
|
def source_files(root):
|
|
root = Path(root).resolve()
|
|
selected = [root / name for name in ROOT_FILES if (root / name).is_file()]
|
|
for folder, suffixes in TREES.items():
|
|
selected.extend(
|
|
path
|
|
for path in (root / folder).rglob("*")
|
|
if path.is_file() and path.suffix in suffixes and "__pycache__" not in path.parts
|
|
)
|
|
for path in selected:
|
|
if path.is_symlink() or not path.resolve().is_relative_to(root):
|
|
raise ValueError(
|
|
f"Export refuses symlinked or external source: {path.relative_to(root)}"
|
|
)
|
|
return sorted(selected)
|
|
|
|
|
|
def export_project(root, destination):
|
|
root = Path(root).resolve()
|
|
destination = Path(destination)
|
|
if destination.is_symlink():
|
|
raise ValueError("Export destination cannot be a symlink")
|
|
destination = destination.resolve()
|
|
if destination == root or any(destination.is_relative_to(root / folder) for folder in TREES):
|
|
raise ValueError("Choose a destination outside the included source folders")
|
|
if destination.exists() and (not destination.is_dir() or any(destination.iterdir())):
|
|
raise ValueError("Export destination must be a new or empty directory")
|
|
files = source_files(root)
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
for source in files:
|
|
target = destination / source.relative_to(root)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, target)
|
|
return len(files)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("destination", type=Path)
|
|
args = parser.parse_args()
|
|
root = Path(__file__).resolve().parents[1]
|
|
try:
|
|
count = export_project(root, args.destination)
|
|
except ValueError as exc:
|
|
parser.error(str(exc))
|
|
print(f"Exported {count} source/documentation files to {args.destination}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|