126 lines
6.0 KiB
Python
Executable File
126 lines
6.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Prepare a writable Void build checkout outside the upstream submodule."""
|
|
import fcntl
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
def git(directory, *arguments):
|
|
return subprocess.check_output(['git', '-C', str(directory), *arguments], text=True).strip()
|
|
|
|
|
|
def inventory(root):
|
|
result = {}
|
|
for path in sorted(root.rglob('*')):
|
|
name = path.relative_to(root).as_posix()
|
|
if path.is_symlink():
|
|
result[name] = ('link', os.readlink(path))
|
|
elif path.is_file():
|
|
with path.open('rb') as stream:
|
|
result[name] = ('file', path.stat().st_mode & 0o777, hashlib.file_digest(stream, 'sha256').hexdigest())
|
|
elif path.is_dir():
|
|
result[name] = ('directory',)
|
|
else:
|
|
raise ValueError(f'Unexpected special source file: {path}')
|
|
return result
|
|
|
|
|
|
def prepare(project):
|
|
source = project / 'vendor/void-packages'
|
|
workspace = project / '.host/void-packages'
|
|
pin = (project / 'VOID_PACKAGES_COMMIT').read_text().strip()
|
|
if not re.fullmatch('[0-9a-f]{40}', pin) or git(source, 'rev-parse', 'HEAD') != pin:
|
|
raise ValueError('Upstream Void checkout differs from VOID_PACKAGES_COMMIT')
|
|
if git(source, 'diff', 'HEAD', '--'):
|
|
raise ValueError('Upstream Void has staged or tracked changes; preserve them outside the submodule first')
|
|
if source.is_symlink() or workspace.is_symlink():
|
|
raise ValueError('Void source and build checkout must be real directories')
|
|
|
|
# Migrate only known generated copies after comparing every entry. Unknown
|
|
# or independently edited source files are never deleted or overwritten.
|
|
overlays = {path.parent.name: path.parent for path in (project / 'packages').glob('*/template')}
|
|
for name in ('hello', 'report'):
|
|
example = project / f'examples/software/{name}/void'
|
|
if (example / 'template').is_file():
|
|
overlays[f'fds-demo-{name}'] = example
|
|
moves = []
|
|
duplicates = []
|
|
known = set()
|
|
for package, original in overlays.items():
|
|
old = source / 'srcpkgs' / package
|
|
if not old.exists() and not old.is_symlink():
|
|
continue
|
|
if git(source, 'ls-tree', 'HEAD', f'srcpkgs/{package}'):
|
|
raise ValueError(f'FDS overlay collides with upstream package: {package}')
|
|
if old.is_symlink() or not old.is_dir() or inventory(old) != inventory(original):
|
|
raise ValueError(f'Preserve independent edits in {old} before migrating it')
|
|
known.add(f'srcpkgs/{package}/')
|
|
new = workspace / 'srcpkgs' / package
|
|
if new.exists() or new.is_symlink():
|
|
if new.is_symlink() or not new.is_dir() or inventory(new) != inventory(original):
|
|
raise ValueError(f'Conflicting generated overlay: {new}')
|
|
duplicates.append(old)
|
|
else:
|
|
moves.append((old, new))
|
|
for name in git(source, 'ls-files', '--others', '--exclude-standard').splitlines():
|
|
if not any(name.startswith(prefix) for prefix in known):
|
|
raise ValueError(f'Untracked upstream file must be moved outside the submodule: {name}')
|
|
for old in [source / 'hostdir', *sorted(source.glob('masterdir-*')), source / 'etc/conf']:
|
|
if not old.exists() and not old.is_symlink():
|
|
continue
|
|
new = workspace / old.relative_to(source)
|
|
if old.is_symlink() or new.exists() or new.is_symlink():
|
|
raise ValueError(f'Cannot migrate {old}: symlink or existing destination {new}; reconcile explicitly')
|
|
if old == source / 'etc/conf' and old.read_bytes() != (project / 'config/xbps-src.conf').read_bytes():
|
|
raise ValueError('Old Void configuration differs from config/xbps-src.conf; reconcile explicitly')
|
|
moves.append((old, new))
|
|
|
|
if not workspace.exists():
|
|
# Independent objects keep this cache usable without Git alternates or
|
|
# hardlinks back into the submodule. No network access is needed.
|
|
with tempfile.TemporaryDirectory(prefix='void-checkout.', dir=workspace.parent) as temporary:
|
|
checkout = Path(temporary) / 'checkout'
|
|
subprocess.run(['git', 'clone', '--quiet', '--no-hardlinks', '--no-checkout', str(source), str(checkout)], check=True)
|
|
subprocess.run(['git', '-C', str(checkout), 'checkout', '--quiet', '--detach', pin], check=True)
|
|
subprocess.run(['git', '-C', str(checkout), 'remote', 'remove', 'origin'], check=True)
|
|
checkout.rename(workspace)
|
|
else:
|
|
if not (workspace / '.git').is_dir() or git(workspace, 'diff', 'HEAD', '--'):
|
|
raise ValueError('Build checkout has tracked changes; preserve them before preparing it')
|
|
if git(workspace, 'rev-parse', 'HEAD') != pin:
|
|
subprocess.run(['git', '-C', str(workspace), 'fetch', '--quiet', '--update-shallow', str(source), pin], check=True)
|
|
# Git refuses an update that would overwrite local files.
|
|
subprocess.run(['git', '-C', str(workspace), 'checkout', '--quiet', '--detach', pin], check=True)
|
|
for old, new in moves:
|
|
new.parent.mkdir(parents=True, exist_ok=True)
|
|
old.rename(new)
|
|
print(f'Moved {old.relative_to(project)} to {new.relative_to(project)}', flush=True)
|
|
for old in duplicates:
|
|
shutil.rmtree(old)
|
|
if git(source, 'status', '--porcelain', '--untracked-files=all'):
|
|
raise ValueError('Upstream submodule is not clean after preparation')
|
|
print('PASS: clean upstream reference; writable build checkout in .host/void-packages', flush=True)
|
|
|
|
|
|
def main():
|
|
project = Path(__file__).resolve().parents[1]
|
|
if len(sys.argv) != 1:
|
|
sys.exit('Usage: tools/prepare-void-workspace')
|
|
try:
|
|
(project / '.host').mkdir(exist_ok=True)
|
|
with (project / '.host/void-workspace.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
prepare(project)
|
|
except (OSError, ValueError, subprocess.CalledProcessError) as error:
|
|
sys.exit(f'ERROR: {error}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|