Repository navigation
Add shared Settings, menu and About code with a surfaces command #26
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "schema": "mac-surfaces/v1", | ||
| "version": 8, | ||
| "destination": "privacycommand/Sources/privacycommand/MacSurfaces", | ||
| "tool": "d667ee5efbd0ba513ddb48397881d97f259c837269e5f45d46a839fbed56350a", | ||
| "files": { | ||
| "SurfaceAbout.swift": "8722b826c6b9e53214a2ab0171ecdddf89907d60f9b17759607f4409b1f3aebc", | ||
| "SurfaceActivation.swift": "48d77775caf80c48a295631bacefa07d8e774888e009f86c1ac04ebbc2439d5b", | ||
| "SurfaceBuild.swift": "e02a17f9bde7ef4a003c0abe0f14fdecfabe8eacbfdeb8708d300a59873edbb6", | ||
| "SurfaceCommands.swift": "70b89a2de563bc1f3f6f835d2c89d5b90a1bce677f24d64dc0cbea722898a9c6", | ||
| "SurfaceManual.swift": "84df6bbeaa06265fb1c3388ced6784920994ca86bf00b5168458610b97af5cd9", | ||
| "SurfaceMenuBar.swift": "02776bb94305f25da89783eb2907872816133a16e417072ffb38c861611ddb2f", | ||
| "SurfaceProfile.swift": "63238bbeb405002770dd72e5150f2a2c74900cbbae7c50c5592af06f74d70cf0", | ||
| "SurfaceRows.swift": "b01b6864f50ce89b94b562cc8b052e3de5370901ab4f7efdd609e0749f2adbed", | ||
| "SurfaceSettings.swift": "7503434a92f6a2a94990f110939037b6b7c948a6f3d2a8efbcda4f5ae460203b", | ||
| "SurfaceShortcuts.swift": "83be738da118854446dc649edb26abcddb162be07f9f6348778e0fa8ebad3c6f", | ||
| "SurfaceSparkle.swift": "1ab6c60a27f34e0fc0551c28b82ef0c456a82e26803aabd98043dd6890ced436", | ||
| "SurfaceUpdates.swift": "99fcb3c8db66cba9549697a56f5538bbf0eff46e9a27714c9628286bee6326e3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| #!/usr/bin/env python3 | ||
| """Copy the shared macOS surface sources into an app, or verify the copy. | ||
|
|
||
| Run from an app as `.project/mac_surfaces.py` (through `just surfaces` and the | ||
| build). When the standards checkout is reachable, a newer reviewed version is | ||
| copied in and shows up as an ordinary diff to commit. When it is not, as in a | ||
| contributor's clone, the committed copy is verified against its lock. CI and | ||
| TestFlight or release builds only ever verify, so they never change sources. | ||
| """ | ||
| import argparse | ||
| import hashlib | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
|
|
||
| SCHEMA = "mac-surfaces/v1" | ||
| LOCK = ".project/mac-surfaces.lock.json" | ||
| TOOL = ".project/mac_surfaces.py" | ||
| STANDARD = Path("dev") / "standards" / "mac-surfaces" | ||
|
|
||
|
|
||
| class SurfaceError(Exception): | ||
| pass | ||
|
|
||
|
|
||
| def digest(path): | ||
| return hashlib.sha256(Path(path).read_bytes()).hexdigest() | ||
|
|
||
|
|
||
| def source_files(source): | ||
| return sorted((source / "Sources" / "MacSurfaces").glob("*.swift")) | ||
|
|
||
|
|
||
| def build_manifest(source, version): | ||
| return {"schema": SCHEMA, "version": version, | ||
| "tool": digest(source / "sync.py"), | ||
| "files": {path.name: digest(path) for path in source_files(source)}} | ||
|
|
||
|
|
||
| def read_json(path): | ||
| return json.loads(Path(path).read_text()) | ||
|
|
||
|
|
||
| def write_json(path, value): | ||
| Path(path).parent.mkdir(parents=True, exist_ok=True) | ||
| Path(path).write_text(json.dumps(value, indent=2) + "\n") | ||
|
|
||
|
|
||
| def find_source(root, explicit): | ||
| """The standards checkout, or None when this machine does not have one.""" | ||
| candidates = [] | ||
| if explicit: | ||
| candidates.append(Path(explicit)) | ||
| if os.environ.get("MAC_SURFACES_SOURCE"): | ||
| candidates.append(Path(os.environ["MAC_SURFACES_SOURCE"])) | ||
| candidates.extend(main_checkout_candidates(root)) | ||
| candidates.append(root.parent / STANDARD) | ||
| for candidate in candidates: | ||
| if (candidate / "manifest.json").is_file(): | ||
| return candidate.resolve() | ||
| if explicit: | ||
| raise SurfaceError(f"No manifest.json under {explicit}") | ||
| return None | ||
|
|
||
|
|
||
| def main_checkout_candidates(root): | ||
| """The standards folder beside the app's main checkout, which a worktree | ||
| of the app shares. Empty when the app is not a git checkout or git is not | ||
| installed; the folder beside the app itself is still tried after this.""" | ||
| try: | ||
| common = subprocess.run(["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], | ||
| cwd=root, capture_output=True, text=True, check=True).stdout.strip() | ||
| except (OSError, subprocess.CalledProcessError): | ||
| return [] | ||
| return [Path(common).parent.parent / STANDARD] if common else [] | ||
|
|
||
|
|
||
| def publishing(): | ||
| """True in CI and in TestFlight or release builds, which must not change sources.""" | ||
| channel = os.environ.get("BUILD_CHANNEL", "") | ||
| return channel in ("ci", "testflight", "release") or bool(os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS")) | ||
|
|
||
|
|
||
| def verify(root, lock): | ||
| """Problems with the committed copy, as a list of sentences.""" | ||
| destination = root / lock["destination"] | ||
| problems = [] | ||
| for name, expected in lock["files"].items(): | ||
| path = destination / name | ||
| if not path.is_file(): | ||
| problems.append(f"{name} is missing") | ||
| elif digest(path) != expected: | ||
| problems.append(f"{name} was edited in this app") | ||
| if destination.is_dir(): | ||
| for path in sorted(destination.glob("*.swift")): | ||
| if path.name not in lock["files"]: | ||
| problems.append(f"{path.name} is not part of the standard") | ||
| return problems | ||
|
|
||
|
|
||
| def check(root): | ||
| lock_path = root / LOCK | ||
| if not lock_path.is_file(): | ||
| raise SurfaceError("No copy recorded; run sync with --destination first") | ||
| lock = read_json(lock_path) | ||
| problems = verify(root, lock) | ||
| if problems: | ||
| raise SurfaceError("The shared surface code has drifted: " + "; ".join(problems) | ||
| + ". Change the standard, then sync; do not edit the copy.") | ||
| print(f"Mac surfaces: version {lock['version']}, {len(lock['files'])} files verified") | ||
|
|
||
|
|
||
| def sync(root, destination, explicit_source, force): | ||
| lock_path = root / LOCK | ||
| lock = read_json(lock_path) if lock_path.is_file() else None | ||
| if destination is None: | ||
| if lock is None: | ||
| raise SurfaceError("First sync needs --destination, the folder in the app that holds the copy") | ||
| destination = lock["destination"] | ||
| if lock is not None and publishing(): | ||
| print("Mac surfaces: CI or publication build; verifying the committed copy without copying") | ||
| check(root) | ||
| return | ||
| source = find_source(root, explicit_source) | ||
| if source is None: | ||
| if lock is None: | ||
| raise SurfaceError("The standards checkout is not on this machine and no copy is recorded") | ||
| print("Mac surfaces: standards checkout not found; using the committed copy") | ||
| check(root) | ||
| return | ||
|
|
||
| manifest = read_json(source / "manifest.json") | ||
| actual = build_manifest(source, manifest["version"]) | ||
| if actual != manifest: | ||
| raise SurfaceError("The standards checkout has changes that manifest.json does not record; " | ||
| "run `python3 sync.py manifest --bump` there") | ||
|
|
||
| if lock is not None: | ||
| problems = verify(root, dict(lock, destination=lock["destination"])) | ||
| if problems and not force: | ||
| raise SurfaceError("The shared surface code has drifted: " + "; ".join(problems) | ||
| + ". Change the standard, then sync (or pass --force to discard the edits).") | ||
| if manifest["version"] < lock["version"]: | ||
| print(f"Mac surfaces: checkout has version {manifest['version']}, older than this app's " | ||
| f"{lock['version']}; keeping the committed copy") | ||
| return | ||
| same = manifest["files"] == lock["files"] and destination == lock["destination"] | ||
| if same and not problems and manifest["tool"] == lock.get("tool"): | ||
| print(f"Mac surfaces: version {lock['version']}, up to date") | ||
| return | ||
| if manifest["version"] == lock["version"] and manifest["files"] != lock["files"]: | ||
| raise SurfaceError("The standard changed without a version bump; " | ||
| "run `python3 sync.py manifest --bump` in the standards checkout") | ||
|
|
||
| target = root / destination | ||
| target.mkdir(parents=True, exist_ok=True) | ||
| previous = set(lock["files"]) if lock else set() | ||
| old_target = root / lock["destination"] if lock else target | ||
| for name in previous - set(manifest["files"]): | ||
| (old_target / name).unlink(missing_ok=True) | ||
| if lock and old_target != target: | ||
| for name in previous & set(manifest["files"]): | ||
| (old_target / name).unlink(missing_ok=True) | ||
| for path in source_files(source): | ||
| shutil.copyfile(path, target / path.name) | ||
| tool = root / TOOL | ||
| if not tool.is_file() or digest(tool) != manifest["tool"]: | ||
| tool.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copyfile(source / "sync.py", tool) | ||
| write_json(lock_path, {"schema": SCHEMA, "version": manifest["version"], "destination": destination, | ||
| "tool": manifest["tool"], "files": manifest["files"]}) | ||
| print(f"Mac surfaces: copied version {manifest['version']} ({len(manifest['files'])} files) " | ||
| f"into {destination}; review and commit the change") | ||
|
|
||
|
|
||
| def write_manifest(source, bump): | ||
| path = source / "manifest.json" | ||
| version = read_json(path)["version"] if path.is_file() else 0 | ||
| current = build_manifest(source, version) | ||
| if path.is_file() and current == read_json(path): | ||
| print(f"Mac surfaces: manifest is current at version {version}") | ||
| return | ||
| if not bump and path.is_file(): | ||
| raise SurfaceError("Sources changed; rerun with --bump to publish them as a new version") | ||
| write_json(path, build_manifest(source, version + 1)) | ||
| print(f"Mac surfaces: manifest written, version {version + 1}") | ||
|
|
||
|
|
||
| def main(argv=None): | ||
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | ||
| parser.add_argument("action", choices=["sync", "check", "manifest"]) | ||
| parser.add_argument("--root", default=".", help="the app checkout (default: current directory)") | ||
| parser.add_argument("--destination", help="folder in the app for the copy, relative to the root") | ||
| parser.add_argument("--source", help="the standard's folder; found automatically when omitted") | ||
| parser.add_argument("--force", action="store_true", help="discard edits made to the copy") | ||
| parser.add_argument("--bump", action="store_true", help="manifest: publish changed sources as a new version") | ||
| options = parser.parse_args(argv) | ||
| try: | ||
| if options.action == "manifest": | ||
| write_manifest(Path(options.source or Path(__file__).parent).resolve(), options.bump) | ||
| elif options.action == "check": | ||
| check(Path(options.root).resolve()) | ||
| else: | ||
| sync(Path(options.root).resolve(), options.destination, options.source, options.force) | ||
| except (SurfaceError, OSError, ValueError, KeyError) as error: | ||
| print("Mac surfaces failed: " + str(error), file=sys.stderr) | ||
| return 1 | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.