Document bundle installation and normalize release directory permissions
This commit is contained in:
1 parent
bfec60848d
commit
ac184576f6
2 files changed
+34
-9
No files matched your search
@@ -50,8 +50,23 @@ def sync_dir(path):
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def ensure_directory(path, mode=0o755):
|
||||
missing = []
|
||||
cursor = path
|
||||
while not cursor.exists():
|
||||
if cursor.is_symlink():
|
||||
fail(f'Refusing dangling directory symlink: {cursor}')
|
||||
missing.append(cursor)
|
||||
cursor = cursor.parent
|
||||
|
||||
for directory in reversed(missing):
|
||||
directory.mkdir(mode=mode)
|
||||
directory.chmod(mode)
|
||||
sync_dir(directory.parent)
|
||||
|
||||
|
||||
def atomic_bytes(path, data, mode=0o644):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(path.parent)
|
||||
fd, temporary = tempfile.mkstemp(prefix='.ferese-', dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as stream:
|
||||
@@ -68,7 +83,7 @@ def atomic_bytes(path, data, mode=0o644):
|
||||
|
||||
|
||||
def atomic_copy(path, source, mode):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(path.parent)
|
||||
fd, temporary = tempfile.mkstemp(prefix='.ferese-', dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as output:
|
||||
@@ -150,10 +165,10 @@ def bundle(args):
|
||||
if output.exists() or output.is_symlink():
|
||||
fail(f'Bundle already exists: {output}')
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(output.parent)
|
||||
with tempfile.TemporaryDirectory(prefix='.bundle-', dir=output.parent) as temporary:
|
||||
staging = Path(temporary) / 'release'
|
||||
staging.mkdir()
|
||||
ensure_directory(staging)
|
||||
files = []
|
||||
for rule in INVENTORY['files']:
|
||||
sources = sorted(source.glob(rule['source']))
|
||||
@@ -210,7 +225,7 @@ class Installation:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def lock(self):
|
||||
self.base.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(self.base)
|
||||
lock_path = self.path(PREFIX + '/.install.lock')
|
||||
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
@@ -339,7 +354,7 @@ class Installation:
|
||||
|
||||
def write_state(self, absolute, state, source=None):
|
||||
path = self.path(absolute)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(path.parent)
|
||||
if state['kind'] == 'absent':
|
||||
path.unlink(missing_ok=True)
|
||||
sync_dir(path.parent)
|
||||
@@ -359,7 +374,7 @@ class Installation:
|
||||
|
||||
def archive(self, journal):
|
||||
history = self.path(PREFIX + '/transactions')
|
||||
history.mkdir(mode=0o700, exist_ok=True)
|
||||
ensure_directory(history, 0o700)
|
||||
os.rename(self.transaction, history / journal['id'])
|
||||
sync_dir(history)
|
||||
sync_dir(self.base)
|
||||
@@ -433,7 +448,7 @@ class Installation:
|
||||
if releases.is_symlink():
|
||||
fail(f'Refusing symlinked release directory: {releases}')
|
||||
|
||||
releases.mkdir(parents=True, exist_ok=True)
|
||||
ensure_directory(releases)
|
||||
destination = releases / manifest['release']
|
||||
if os.path.lexists(destination):
|
||||
if self.manifest(manifest['release']) != manifest:
|
||||
@@ -443,7 +458,7 @@ class Installation:
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='.stage-', dir=releases) as temporary:
|
||||
staging = Path(temporary) / 'release'
|
||||
staging.mkdir()
|
||||
ensure_directory(staging)
|
||||
for item in manifest['files']:
|
||||
atomic_copy(staging / item['target'], origin / item['target'], int(item['mode'], 8))
|
||||
|
||||
|
||||
@@ -224,6 +224,16 @@ m.main()
|
||||
self.assertIn('in progress', result.stderr)
|
||||
self.assertFalse((self.base / 'current').exists())
|
||||
|
||||
def test_restrictive_umask_does_not_make_installed_release_private(self):
|
||||
result = subprocess.run([sys.executable, '-c',
|
||||
f"import os, runpy; os.umask(0o077); runpy.run_path({str(HELPER)!r}, run_name='__main__')",
|
||||
'install', str(self.bundles[0]), '--root', str(self.root)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for relative in ('usr', 'usr/local', PREFIX, PREFIX + '/releases', PREFIX + '/releases/first',
|
||||
PREFIX + '/releases/first/wallpapers', 'usr/share/wayland-sessions'):
|
||||
self.assertEqual((self.root / relative).stat().st_mode & 0o777, 0o755, relative)
|
||||
|
||||
def test_dry_run_does_not_write(self):
|
||||
before = self.snapshot()
|
||||
self.run_command('install', self.bundles[0], '--dry-run')
|
||||
|
||||
Reference in new issue
Block a user