Phase 30: complete configurable installation and release qualification
This commit is contained in:
parent
afc049e2c8
commit
90eac33a71
18 changed files with 399 additions and 59 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"""Deterministic source archive using indexed files, or an exported manifest."""
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
|
@ -70,7 +71,9 @@ def archive(root, output):
|
|||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
print(output)
|
||||
sha = hashlib.sha256(output.read_bytes()).hexdigest()
|
||||
output.with_name(output.name + '.sha256').write_text(f'{sha} {output.name}\n')
|
||||
print(f'{output}: {sha}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
38
tools/install.py
Normal file
38
tools/install.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Install the explicit owned inventory; replace files without following symlinks."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
from install_layout import arguments, inventory, layout, staged
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
arguments(parser)
|
||||
parser.add_argument('--build-dir', type=Path, required=True)
|
||||
parser.add_argument('--gui', action='store_true')
|
||||
args = parser.parse_args()
|
||||
source = Path(__file__).resolve().parent.parent
|
||||
entries = []
|
||||
for destination, original in inventory(source, layout(args)):
|
||||
name = destination.name
|
||||
if name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')):
|
||||
continue
|
||||
if original is None:
|
||||
original = args.build_dir / name
|
||||
destination = staged(destination, args.destdir)
|
||||
if not original.is_file():
|
||||
raise ValueError(f'Missing install input: {original}')
|
||||
if destination.exists() and not (destination.is_file() or destination.is_symlink()):
|
||||
raise ValueError(f'Refusing non-file destination: {destination}')
|
||||
entries.append((destination, original))
|
||||
for destination, original in entries:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, pending = tempfile.mkstemp(prefix='.xtc-install-', dir=destination.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file:
|
||||
shutil.copyfileobj(input_file, output)
|
||||
os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check') else 0o644)
|
||||
os.replace(pending, destination)
|
||||
finally:
|
||||
Path(pending).unlink(missing_ok=True)
|
||||
55
tools/install_layout.py
Normal file
55
tools/install_layout.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Shared install inventory and configurable POSIX directory layout."""
|
||||
from pathlib import Path
|
||||
|
||||
KEYS = ('bindir', 'libdir', 'includedir', 'docdir', 'licensedir', 'examplesdir', 'desktopdir', 'pkgconfigdir')
|
||||
|
||||
def arguments(parser):
|
||||
parser.add_argument('--prefix', default='/usr/local')
|
||||
parser.add_argument('--destdir', default='')
|
||||
for key in KEYS:
|
||||
parser.add_argument('--' + key)
|
||||
|
||||
|
||||
def layout(args):
|
||||
prefix = directory(args.prefix)
|
||||
defaults = dict(bindir=prefix / 'bin', libdir=prefix / 'lib', includedir=prefix / 'include',
|
||||
docdir=prefix / 'share/doc/xtc', licensedir=prefix / 'share/licenses/xtc',
|
||||
examplesdir=prefix / 'share/xtc/examples', desktopdir=prefix / 'share/applications',
|
||||
pkgconfigdir=prefix / 'lib/pkgconfig')
|
||||
return {key: directory(getattr(args, key) or value) for key, value in defaults.items()}
|
||||
|
||||
|
||||
def directory(value):
|
||||
text = str(value)
|
||||
path = Path(text)
|
||||
if not path.is_absolute() or '..' in path.parts or any(c in text for c in '\n\r\0"$\\'):
|
||||
raise ValueError('Install directories must be absolute without .., controls, quotes, $, or backslashes')
|
||||
return path
|
||||
|
||||
|
||||
def inventory(source, paths):
|
||||
entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None),
|
||||
('bindir', 'xtc-check', None), ('bindir', 'xtc', None),
|
||||
('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'),
|
||||
('licensedir', 'LICENSE', source / 'LICENSE'),
|
||||
('docdir', 'README.md', source / 'README.md'),
|
||||
('docdir', 'BINARY_MANIFEST.json', None)]
|
||||
entries += [('includedir', 'xtc/' + p.name, p) for p in sorted((source / 'include/xtc').glob('*.h'))]
|
||||
entries += [('docdir', p.name, p) for p in sorted((source / 'docs').glob('*.md'))]
|
||||
entries += [('docdir', p.name, p) for p in sorted((source / 'docs').glob('*performance*.json'))]
|
||||
entries += [('docdir', 'decisions/' + p.name, p) for p in sorted((source / 'docs/decisions').glob('*.md'))]
|
||||
entries += [('examplesdir', p.name, p) for p in sorted((source / 'examples/projects').glob('*')) if p.is_file()]
|
||||
result = [(paths[key] / name, original) for key, name, original in entries]
|
||||
if len({path for path, _ in result}) != len(result):
|
||||
raise ValueError('Install layout has overlapping file destinations')
|
||||
return result
|
||||
|
||||
|
||||
def staged(path, destdir):
|
||||
if not destdir:
|
||||
return path
|
||||
root = directory(destdir).resolve()
|
||||
result = root / path.relative_to('/')
|
||||
if not result.parent.resolve().is_relative_to(root):
|
||||
raise ValueError(f'Install directory symlink escapes DESTDIR: {result}')
|
||||
return result
|
||||
|
|
@ -3,14 +3,23 @@
|
|||
import argparse
|
||||
from pathlib import Path
|
||||
from dist import version
|
||||
from install_layout import directory
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--prefix', required=True)
|
||||
parser.add_argument('--exec-prefix')
|
||||
parser.add_argument('--libdir')
|
||||
parser.add_argument('--includedir')
|
||||
parser.add_argument('--jack', default='')
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if not args.prefix.startswith('/') or any(c in args.prefix for c in '\n\r\0"$\\'):
|
||||
parser.error('PREFIX must be absolute, without newlines, quotes, $, or backslashes')
|
||||
text = Path('packaging/xtc.pc.in').read_text().replace('@PREFIX@', args.prefix)
|
||||
prefix = directory(args.prefix)
|
||||
exec_prefix = directory(args.exec_prefix or prefix)
|
||||
libdir = directory(args.libdir or exec_prefix / 'lib')
|
||||
includedir = directory(args.includedir or prefix / 'include')
|
||||
def reference(path):
|
||||
return '${prefix}/' + str(path.relative_to(prefix)) if path != prefix and prefix in path.parents else str(path)
|
||||
text = Path('packaging/xtc.pc.in').read_text().replace('@PREFIX@', str(prefix))
|
||||
text = text.replace('@EXEC_PREFIX@', reference(exec_prefix)).replace('@LIBDIR@', reference(libdir)).replace('@INCLUDEDIR@', reference(includedir))
|
||||
text = text.replace('@VERSION@', version(Path.cwd())).replace('@JACK@', args.jack)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(text)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Remove only this source version's installed files; never recurse directories."""
|
||||
"""Remove only this source version's inventory; never recurse directories."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import stat
|
||||
from install_layout import arguments, inventory, layout, staged
|
||||
|
||||
|
||||
def owned_files(source):
|
||||
files = ['lib/libxtc.a', 'lib/pkgconfig/xtc.pc', 'bin/xtc-check', 'bin/xtc',
|
||||
'share/applications/xtc.desktop', 'share/licenses/xtc/LICENSE',
|
||||
'share/doc/xtc/README.md', 'share/doc/xtc/performance-baseline.json',
|
||||
'share/doc/xtc/BINARY_MANIFEST.json']
|
||||
files += ['include/xtc/' + p.name for p in sorted((source / 'include/xtc').glob('*.h'))]
|
||||
files += ['share/doc/xtc/' + p.name for p in sorted((source / 'docs').glob('*.md'))]
|
||||
files += ['share/doc/xtc/decisions/' + p.name for p in sorted((source / 'docs/decisions').glob('*.md'))]
|
||||
return files
|
||||
|
||||
|
||||
def uninstall(root, source):
|
||||
if not root.is_absolute() or root == Path('/'):
|
||||
raise ValueError('Installation root must be absolute and not /')
|
||||
files = [root / name for name in owned_files(source)]
|
||||
# Preflight before changing anything; never unlink a directory at a file path.
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
arguments(parser)
|
||||
parser.add_argument('--root', help='legacy alias for --prefix')
|
||||
args = parser.parse_args()
|
||||
if args.root:
|
||||
args.prefix = args.root
|
||||
files = [staged(path, args.destdir) for path, _ in inventory(Path(__file__).resolve().parent.parent, layout(args))]
|
||||
for path in files:
|
||||
try:
|
||||
mode = path.lstat().st_mode
|
||||
|
|
@ -30,16 +23,14 @@ def uninstall(root, source):
|
|||
raise ValueError(f'Refusing non-file: {path}')
|
||||
for path in files:
|
||||
path.unlink(missing_ok=True)
|
||||
parents = {parent for path in files for parent in path.parents if parent == root or root in parent.parents}
|
||||
# Only try the immediate owned directories, never walk up to / or DESTDIR.
|
||||
parents = {p.parent for p in files}
|
||||
for parent in sorted(parents, key=lambda p: len(p.parts), reverse=True):
|
||||
try:
|
||||
parent.rmdir()
|
||||
except (FileNotFoundError, OSError):
|
||||
pass # Shared/nonempty directories remain untouched.
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--root', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
uninstall(args.root, Path(__file__).resolve().parent.parent)
|
||||
main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue