Repository navigation
Preserve Lake declarations and traces when vendoring #3875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,26 @@ | |||||
| REMOTE_TOML_KEYS = {"git", "url", "rev", "scope", "subDir", "inputRev"} | ||||||
|
|
||||||
|
|
||||||
| def validate_vendoring_roots(root: Path, packages_dir: Path) -> None: | ||||||
| """Reject links that could route a private rewrite into another installation.""" | ||||||
| roots = [root.absolute(), packages_dir.absolute()] | ||||||
| for directory in roots: | ||||||
| if directory.is_symlink() or not directory.is_dir(): | ||||||
| raise ValueError(f"Vendoring root must be a physical directory: {directory}") | ||||||
| allowed = [directory.resolve() for directory in roots] | ||||||
| def fail_walk(error: OSError) -> None: | ||||||
| raise error | ||||||
|
|
||||||
| for directory in roots: | ||||||
| for current, directories, files in os.walk(directory, followlinks=False, onerror=fail_walk): | ||||||
| for name in directories + files: | ||||||
| path = Path(current) / name | ||||||
| if path.is_symlink(): | ||||||
| target = path.resolve(strict=True) | ||||||
| if not any(target.is_relative_to(parent) for parent in allowed): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| raise ValueError(f"Vendoring symlink leaves private roots: {path} -> {target}") | ||||||
|
|
||||||
|
|
||||||
| def quote_name(name: str) -> str: | ||||||
| if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_']*", name): | ||||||
| return name | ||||||
|
|
@@ -34,7 +54,9 @@ def package_dirs(packages_dir: Path) -> dict[str, Path]: | |||||
|
|
||||||
| def replace_require_toml_blocks(path: Path, packages: dict[str, Path]) -> bool: | ||||||
| content = path.read_text() | ||||||
| block_re = re.compile(r"(?ms)^\s*\[\[require\]\]\s*\n(?:(?!^\s*\[\[).*\n?)*") | ||||||
| # Stop at every TOML table, including single-bracket option tables. A | ||||||
| # greedy dot with DOTALL would also consume later library/executable tables. | ||||||
| block_re = re.compile(r"(?ms)^[ \t]*\[\[require\]\][ \t]*\n.*?(?=^[ \t]*\[|\Z)") | ||||||
|
|
||||||
| changed = False | ||||||
|
|
||||||
|
|
@@ -151,7 +173,7 @@ def rewrite_trace_prefixes( | |||||
| prefixes = trace_prefixes(root, packages_dir, packages, extra_prefixes) | ||||||
| package_root_patterns = [ | ||||||
| re.compile( | ||||||
| rf"(?<![A-Za-z0-9_.-])(?:/[^\s\"':]+)+/(?:packages|\.lake/packages)/{re.escape(name)}/" | ||||||
| rf"(?<![A-Za-z0-9_.-])(?:/[^/\s\"':]+)+/(?:packages|\.lake/packages)/{re.escape(name)}/" | ||||||
| ) | ||||||
| for name in packages | ||||||
| ] | ||||||
|
|
@@ -215,6 +237,7 @@ def main() -> None: | |||||
| ) | ||||||
| args = parser.parse_args() | ||||||
|
|
||||||
| validate_vendoring_roots(args.root, args.packages_dir) | ||||||
| root = args.root.resolve() | ||||||
| packages_dir = args.packages_dir.resolve() | ||||||
| packages = package_dirs(packages_dir) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| These unmodified Lake configurations are regression inputs for vendoring, not | ||
| dependency pins. They come from the dependency revisions in Anneal's pinned | ||
| Aeneas archive: | ||
|
|
||
| - `aesop.toml`: aesop `e3cb2f741431ce31bf73549fb52316a57368b06f`. | ||
| - `importGraph.toml`: import-graph `5c7542ed018c78194f1e2b903eaf6a792b74c03d`. | ||
|
|
||
| Both projects distribute these files under Apache-2.0; see the repository's | ||
| `LICENSE-APACHE`. The regression checks that replacing a remote dependency does | ||
| not remove the library and executable declarations that follow it. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| name = "aesop" | ||
| defaultTargets = ["Aesop"] | ||
| testDriver = "AesopTest" | ||
| precompileModules = false # We would like to turn this on, but it breaks the Mathlib cache. | ||
| platformIndependent = true | ||
|
|
||
| [[require]] | ||
| name = "batteries" | ||
| scope = "leanprover-community" | ||
| rev = "v4.31.0" | ||
|
|
||
| [[lean_lib]] | ||
| name = "Aesop" | ||
|
|
||
| [[lean_lib]] | ||
| name = "AesopTest" | ||
| globs = ["AesopTest.+"] | ||
| leanOptions = {linter.unusedVariables = false} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| name = "importGraph" | ||
| description = "Tools to analyse and visualise the import structure of Lean packages and their files." | ||
| license = "Apache-2.0" | ||
| licenseFiles = ["LICENSE"] | ||
|
|
||
| defaultTargets = ["ImportGraph", "graph"] | ||
| testRunner = "ImportGraphTest" | ||
|
|
||
| [[require]] | ||
| name = "Cli" | ||
| scope = "leanprover" | ||
| rev = "v4.31.0" | ||
|
|
||
| [[lean_lib]] | ||
| name = "ImportGraph" | ||
|
|
||
| # `lake exe graph` constructs import graphs in `.dot` or graphical formats. | ||
| [[lean_exe]] | ||
| name = "graph" | ||
| root = "MainGraph" | ||
| # Enables the use of the Lean interpreter by the executable (e.g., `runFrontend`) | ||
| # at the expense of increased binary size on Linux. | ||
| # Remove this line if you do not need such functionality. | ||
| supportInterpreter = true | ||
|
|
||
| # `lake exe unused_transitive_imports` prints unused transitive imports from amongst a given list of modules. | ||
| [[lean_exe]] | ||
| name = "unused_transitive_imports" | ||
| root = "MainUnusedTransitiveImports" | ||
| supportInterpreter = true | ||
|
|
||
| [[lean_lib]] | ||
| name = "ImportGraphTest" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
strict=Trueinpath.resolve(strict=True)will raise aFileNotFoundErrorif a broken or dangling symlink is encountered. It is common for repositories or vendored packages to contain dangling symlinks (e.g., in test fixtures or unused files). If this happens, the script will crash with a traceback instead of handling it gracefully.\n\nUsingstrict=False(which is the default forresolve()) allows the path to be resolved as much as possible. If the resolved path points outside the allowed roots, it will still be correctly caught and raise the descriptiveValueError. If it points to a nonexistent path inside the allowed roots, it will be safely allowed.