Some checks failed
Research feed publisher tests / test (push) Failing after 3s
Release / Pre-flight checks (push) Failing after 0s
Release / Research Edition — patch and packaging smoke tests (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, false, false, false) (push) Has been skipped
Release / Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor) (22, 3.9) (push) Has been skipped
Release / Generate release notes (push) Has been skipped
Release / Create dev release tag (push) Has been skipped
CodeQL / Analyze (javascript) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Release / Build Qt artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04-arm, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Publish draft release (push) Has been cancelled
281 lines
9.3 KiB
Python
Executable File
281 lines
9.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Verify that Qt and Tauri bundle the same aw-server-rust revision."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
|
|
|
|
FIELD_RE = re.compile(r'^\s*([a-zA-Z0-9_-]+)\s*=\s*"([^"]+)"\s*$', re.MULTILINE)
|
|
VERSION_RE = re.compile(r"^(\d+)\.(\d+)(?:\.|-|$)")
|
|
REVISION_RE = re.compile(r"#([0-9a-f]{40})$")
|
|
|
|
|
|
def major_minor(version: str) -> str:
|
|
match = VERSION_RE.match(version)
|
|
if not match:
|
|
raise ValueError(f"invalid version: {version!r}")
|
|
return ".".join(match.groups())
|
|
|
|
|
|
def read_package_version(cargo_toml: Path) -> str:
|
|
in_package = False
|
|
for line in cargo_toml.read_text(encoding="utf-8").splitlines():
|
|
if line.strip() == "[package]":
|
|
in_package = True
|
|
continue
|
|
if in_package and line.startswith("["):
|
|
break
|
|
if in_package:
|
|
match = re.match(r'^version\s*=\s*"([^"]+)"', line)
|
|
if match:
|
|
return match.group(1)
|
|
raise ValueError(f"no [package].version in {cargo_toml}")
|
|
|
|
|
|
def read_locked_server(cargo_lock: Path) -> Tuple[str, str]:
|
|
matches = []
|
|
for block in re.split(
|
|
r"(?m)^\[\[package\]\]\s*$", cargo_lock.read_text(encoding="utf-8")
|
|
):
|
|
fields = dict(FIELD_RE.findall(block))
|
|
source = fields.get("source", "")
|
|
if (
|
|
fields.get("name") == "aw-server"
|
|
and "ActivityWatch/aw-server-rust" in source
|
|
):
|
|
revision = REVISION_RE.search(source)
|
|
if not revision:
|
|
raise ValueError(
|
|
f"aw-server source has no full Git revision: {source!r}"
|
|
)
|
|
matches.append((fields["version"], revision.group(1)))
|
|
|
|
if len(matches) != 1:
|
|
raise ValueError(
|
|
f"expected one Git-locked aw-server package in {cargo_lock}, found {len(matches)}"
|
|
)
|
|
return matches[0]
|
|
|
|
|
|
def validation_errors(
|
|
release_version: Optional[str],
|
|
submodule_version: str,
|
|
submodule_revision: str,
|
|
tauri_version: str,
|
|
tauri_revision: str,
|
|
) -> List[str]:
|
|
errors = []
|
|
if release_version is not None:
|
|
release_line = major_minor(release_version)
|
|
if major_minor(submodule_version) != release_line:
|
|
errors.append(
|
|
f"aw-server-rust submodule version {submodule_version} does not match "
|
|
f"release line {release_line}"
|
|
)
|
|
if major_minor(tauri_version) != release_line:
|
|
errors.append(
|
|
f"Tauri locks aw-server {tauri_version}, which does not match release line "
|
|
f"{release_line}"
|
|
)
|
|
if tauri_revision != submodule_revision:
|
|
errors.append(
|
|
f"Tauri locks aw-server-rust {tauri_revision[:12]}, but the release "
|
|
f"submodule is {submodule_revision[:12]}"
|
|
)
|
|
return errors
|
|
|
|
|
|
def is_git_checkout(path: Path) -> bool:
|
|
"""Return whether path is the root of an initialized Git checkout."""
|
|
try:
|
|
git_root = Path(
|
|
subprocess.check_output(
|
|
["git", "-C", str(path), "rev-parse", "--show-toplevel"],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
).strip()
|
|
).resolve()
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
return git_root == path.resolve()
|
|
|
|
|
|
def initialize_submodule(root: Path, name: str) -> bool:
|
|
"""Initialize a missing submodule without resetting an updated checkout."""
|
|
path = root / name
|
|
if is_git_checkout(path):
|
|
expected_git_dir = Path(
|
|
subprocess.check_output(
|
|
["git", "-C", str(root), "rev-parse", "--git-path", f"modules/{name}"],
|
|
text=True,
|
|
).strip()
|
|
)
|
|
if not expected_git_dir.is_absolute():
|
|
expected_git_dir = root / expected_git_dir
|
|
expected_git_dir = expected_git_dir.resolve()
|
|
actual_git_dir = Path(
|
|
subprocess.check_output(
|
|
["git", "-C", str(path), "rev-parse", "--absolute-git-dir"],
|
|
text=True,
|
|
).strip()
|
|
).resolve()
|
|
if actual_git_dir != expected_git_dir:
|
|
hint = ""
|
|
if (path / ".git").is_dir():
|
|
hint = (
|
|
f"; if this is an old-form submodule, migrate it first with "
|
|
f"'git submodule absorbgitdirs {name}'"
|
|
)
|
|
raise ValueError(
|
|
f"refusing unmanaged Git checkout at configured submodule path {path}"
|
|
f"{hint}"
|
|
)
|
|
return False
|
|
subprocess.run(
|
|
["git", "-C", str(root), "submodule", "update", "--init", name], check=True
|
|
)
|
|
return True
|
|
|
|
|
|
def sync_submodule(server_root: Path, revision: str) -> bool:
|
|
"""Check out the Tauri-locked revision in the top-level server submodule."""
|
|
if not is_git_checkout(server_root):
|
|
raise ValueError(
|
|
f"aw-server-rust is not initialized as a Git submodule at {server_root}"
|
|
)
|
|
|
|
dirty = subprocess.check_output(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(server_root),
|
|
"status",
|
|
"--porcelain",
|
|
"--untracked-files=no",
|
|
"--ignore-submodules=all",
|
|
],
|
|
text=True,
|
|
).strip()
|
|
if dirty:
|
|
raise ValueError(
|
|
f"refusing to replace dirty aw-server-rust checkout at {server_root}"
|
|
)
|
|
|
|
current_revision = subprocess.check_output(
|
|
["git", "-C", str(server_root), "rev-parse", "HEAD"], text=True
|
|
).strip()
|
|
if current_revision == revision:
|
|
return False
|
|
|
|
try:
|
|
subprocess.run(
|
|
["git", "-C", str(server_root), "cat-file", "-e", f"{revision}^{{commit}}"],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
subprocess.run(
|
|
["git", "-C", str(server_root), "fetch", "origin", revision], check=True
|
|
)
|
|
|
|
subprocess.run(
|
|
["git", "-C", str(server_root), "checkout", "--detach", revision], check=True
|
|
)
|
|
subprocess.run(
|
|
["git", "-C", str(server_root), "submodule", "update", "--init", "--recursive"],
|
|
check=True,
|
|
)
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"release_version",
|
|
nargs="?",
|
|
help="ActivityWatch version without the leading v (required unless --sync)",
|
|
)
|
|
parser.add_argument(
|
|
"--sync",
|
|
action="store_true",
|
|
help="make the top-level aw-server-rust submodule follow Tauri's Cargo.lock",
|
|
)
|
|
parser.add_argument(
|
|
"--repo-root", type=Path, default=Path(__file__).resolve().parents[1]
|
|
)
|
|
args = parser.parse_args()
|
|
if args.release_version is None and not args.sync:
|
|
parser.error("release_version is required unless --sync is used")
|
|
|
|
root = args.repo_root.resolve()
|
|
server_root = root / "aw-server-rust"
|
|
try:
|
|
if args.sync:
|
|
initialize_submodule(root, "aw-tauri")
|
|
initialize_submodule(root, "aw-server-rust")
|
|
tauri_version, tauri_revision = read_locked_server(
|
|
root / "aw-tauri/src-tauri/Cargo.lock"
|
|
)
|
|
if args.sync:
|
|
changed = sync_submodule(server_root, tauri_revision)
|
|
if changed:
|
|
print(f"Synced aw-server-rust to Tauri lock {tauri_revision[:12]}")
|
|
else:
|
|
print(f"aw-server-rust already matches Tauri lock {tauri_revision[:12]}")
|
|
submodule_version = read_package_version(server_root / "aw-server/Cargo.toml")
|
|
submodule_revision = subprocess.check_output(
|
|
["git", "-C", str(server_root), "rev-parse", "HEAD"], text=True
|
|
).strip()
|
|
errors = validation_errors(
|
|
args.release_version,
|
|
submodule_version,
|
|
submodule_revision,
|
|
tauri_version,
|
|
tauri_revision,
|
|
)
|
|
except (OSError, subprocess.CalledProcessError, ValueError) as exc:
|
|
print(
|
|
f"ERROR: could not inspect bundled aw-server versions: {exc}",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
if args.release_version is not None:
|
|
print(f"ActivityWatch release: {args.release_version}")
|
|
print(f"aw-server-rust: {submodule_version} @ {submodule_revision[:12]}")
|
|
print(f"Tauri Cargo.lock: {tauri_version} @ {tauri_revision[:12]}")
|
|
if errors:
|
|
print("\nERROR: inconsistent aw-server build inputs:", file=sys.stderr)
|
|
for error in errors:
|
|
print(f" - {error}", file=sys.stderr)
|
|
if (
|
|
args.release_version is not None
|
|
and major_minor(tauri_version) != major_minor(args.release_version)
|
|
):
|
|
print(
|
|
"\nUpdate aw-tauri's Cargo.lock to the intended server release line, "
|
|
"then run `make sync-tauri-server`.",
|
|
file=sys.stderr,
|
|
)
|
|
else:
|
|
print(
|
|
"\nRun `make sync-tauri-server` to make the top-level submodule "
|
|
"follow aw-tauri's Cargo.lock.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
print("OK: Qt and Tauri will bundle the same aw-server-rust revision")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|