Initial server source import
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
"""Build reproducible remote-worker package trees from pinned public inputs."""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_DIR = os.path.dirname(APP_DIR)
|
||||
DEPENDENCIES_DIR = os.path.join(APP_DIR, 'dependencies')
|
||||
if os.path.isdir(DEPENDENCIES_DIR) and DEPENDENCIES_DIR not in sys.path:
|
||||
sys.path.insert(0, DEPENDENCIES_DIR)
|
||||
if APP_DIR not in sys.path:
|
||||
sys.path.insert(0, APP_DIR)
|
||||
|
||||
from lifecycle_authority import REMOTE_WORKER_CODE_AUTHORITY_FILES
|
||||
from runtime_security import harden_private_tree, reject_reparse_components, sha256_file
|
||||
from worker_package import (
|
||||
DEFAULT_WORKER_PACKAGE_CAPABILITIES,
|
||||
build_worker_package_manifest,
|
||||
verify_worker_package,
|
||||
worker_package_manifest_sha256,
|
||||
write_worker_package_manifest,
|
||||
)
|
||||
|
||||
|
||||
class WorkerPackageBuildError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _regular_file(path, label):
|
||||
path = os.path.abspath(os.fspath(path))
|
||||
reject_reparse_components(path)
|
||||
details = os.stat(path, follow_symlinks=False)
|
||||
if not stat.S_ISREG(details.st_mode) or os.path.islink(path):
|
||||
raise WorkerPackageBuildError(f'{label} is not a regular file')
|
||||
return path
|
||||
|
||||
|
||||
def _copy_file(source, destination, label):
|
||||
source = _regular_file(source, label)
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
shutil.copymode(source, destination, follow_symlinks=False)
|
||||
return destination
|
||||
|
||||
|
||||
def _copy_tree(source, destination, label):
|
||||
source = os.path.abspath(os.fspath(source))
|
||||
reject_reparse_components(source)
|
||||
if not os.path.isdir(source) or os.path.islink(source):
|
||||
raise WorkerPackageBuildError(f'{label} is not a directory')
|
||||
os.makedirs(destination, exist_ok=False)
|
||||
copied = 0
|
||||
for current, directories, names in os.walk(source, followlinks=False):
|
||||
relative = os.path.relpath(current, source)
|
||||
target = destination if relative == '.' else os.path.join(destination, relative)
|
||||
for name in list(directories):
|
||||
path = os.path.join(current, name)
|
||||
reject_reparse_components(path)
|
||||
if os.path.islink(path) or name.lower() == '__pycache__':
|
||||
raise WorkerPackageBuildError(f'{label} contains an unsupported directory')
|
||||
os.makedirs(os.path.join(target, name), exist_ok=False)
|
||||
for name in names:
|
||||
if name.lower().endswith(('.pyc', '.pyo')):
|
||||
raise WorkerPackageBuildError(f'{label} contains cached bytecode')
|
||||
_copy_file(
|
||||
os.path.join(current, name), os.path.join(target, name),
|
||||
f'{label} file',
|
||||
)
|
||||
copied += 1
|
||||
if copied == 0:
|
||||
raise WorkerPackageBuildError(f'{label} is empty')
|
||||
return copied
|
||||
|
||||
|
||||
def _windows_support_files(root):
|
||||
launcher = (
|
||||
'@echo off\n'
|
||||
'"%~dp0runtime\\python\\python.exe" -u -I -S -B '
|
||||
'"%~dp0app\\remote_worker_bootstrap.py" -- %*\n'
|
||||
)
|
||||
with open(os.path.join(root, 'truf-worker.cmd'), 'w', encoding='ascii', newline='\r\n') as handle:
|
||||
handle.write(launcher)
|
||||
with open(os.path.join(root, 'run-worker.cmd'), 'w', encoding='ascii', newline='\r\n') as handle:
|
||||
handle.write(
|
||||
'@echo off\n'
|
||||
'"%~dp0runtime\\python\\python.exe" -u -I -S -B '
|
||||
'"%~dp0app\\remote_worker_bootstrap.py" -- run %*\n'
|
||||
)
|
||||
with open(os.path.join(root, 'prepare-worker.ps1'), 'w', encoding='ascii', newline='\r\n') as handle:
|
||||
handle.write(
|
||||
"$ErrorActionPreference = 'Stop'\n"
|
||||
"$root = (Resolve-Path -LiteralPath $PSScriptRoot).Path\n"
|
||||
"$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value\n"
|
||||
"& icacls.exe $root /inheritance:r /grant:r "
|
||||
"\"*$sid`:(OI)(CI)F\" \"*S-1-5-18`:(OI)(CI)F\" "
|
||||
"\"*S-1-5-32-544`:(OI)(CI)F\" | Out-Null\n"
|
||||
"if ($LASTEXITCODE -ne 0) { throw 'worker package ACL preparation failed' }\n"
|
||||
"& icacls.exe (Join-Path $root '*') /inheritance:d /T /C | Out-Null\n"
|
||||
"if ($LASTEXITCODE -ne 0) { throw 'worker package child ACL preparation failed' }\n"
|
||||
)
|
||||
|
||||
|
||||
def _linux_support_files(root):
|
||||
launcher = (
|
||||
'#!/bin/sh\n'
|
||||
'set -eu\n'
|
||||
'root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\n'
|
||||
'exec python3 -u -I -S -B "$root/app/remote_worker_bootstrap.py" -- "$@"\n'
|
||||
)
|
||||
for name, arguments in (('truf-worker', ''), ('run-worker', 'run ')):
|
||||
path = os.path.join(root, name)
|
||||
with open(path, 'w', encoding='ascii', newline='\n') as handle:
|
||||
handle.write(launcher.replace('-- "$@"', f'-- {arguments}"$@"'))
|
||||
os.chmod(path, 0o755)
|
||||
prepare = (
|
||||
'#!/bin/sh\n'
|
||||
'set -eu\n'
|
||||
'test "$(id -u)" -eq 0 || { echo "prepare-worker.sh requires sudo" >&2; exit 1; }\n'
|
||||
'test -n "${SUDO_UID:-}" && test -n "${SUDO_GID:-}" && test "$SUDO_UID" -ne 0 || '
|
||||
'{ echo "run prepare-worker.sh through sudo as the worker user" >&2; exit 1; }\n'
|
||||
'root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\n'
|
||||
'test -z "$(find "$root" -type l -print -quit)" || '
|
||||
'{ echo "worker package contains a symbolic link" >&2; exit 1; }\n'
|
||||
'chown 0:0 "$root"\n'
|
||||
'chmod 0755 "$root"\n'
|
||||
'chown -R "$SUDO_UID:$SUDO_GID" "$root/app"\n'
|
||||
'find "$root/app" -type d -exec chmod 0700 {} +\n'
|
||||
'find "$root/app" -type f -exec chmod 0600 {} +\n'
|
||||
'chown "$SUDO_UID:$SUDO_GID" "$root/worker-package.json"\n'
|
||||
'chmod 0600 "$root/worker-package.json"\n'
|
||||
'chown -R 0:0 "$root/bin" "$root/runtime"\n'
|
||||
'find "$root/bin" "$root/runtime" -type d -exec chmod 0755 {} +\n'
|
||||
'find "$root/bin" "$root/runtime" -type f -exec chmod go-w {} +\n'
|
||||
'chown 0:0 "$root/truf-worker" "$root/run-worker" "$root/prepare-worker.sh"\n'
|
||||
'chmod 0755 "$root/truf-worker" "$root/run-worker" "$root/prepare-worker.sh"\n'
|
||||
)
|
||||
prepare_path = os.path.join(root, 'prepare-worker.sh')
|
||||
with open(prepare_path, 'w', encoding='ascii', newline='\n') as handle:
|
||||
handle.write(prepare)
|
||||
os.chmod(prepare_path, 0o755)
|
||||
|
||||
|
||||
def assemble_worker_package(
|
||||
package_root, *, source_app, dependencies_root, detector_policy_source,
|
||||
trufflehog_source, git_source_root, git_executable, platform_tag,
|
||||
operator_readme_source, python_source_root=None, build_inputs_path=None,
|
||||
operator_cheatsheet_sources=(),
|
||||
capabilities=DEFAULT_WORKER_PACKAGE_CAPABILITIES,
|
||||
):
|
||||
package_root = os.path.abspath(os.fspath(package_root))
|
||||
parent = os.path.dirname(package_root)
|
||||
if os.path.lexists(package_root):
|
||||
raise WorkerPackageBuildError('worker package destination already exists')
|
||||
if not os.path.isdir(parent):
|
||||
raise WorkerPackageBuildError('worker package destination parent is absent')
|
||||
staging = tempfile.mkdtemp(prefix='.truf-worker-build-', dir=parent)
|
||||
try:
|
||||
app_root = os.path.join(staging, 'app')
|
||||
os.makedirs(app_root)
|
||||
source_app = os.path.abspath(os.fspath(source_app))
|
||||
for name in REMOTE_WORKER_CODE_AUTHORITY_FILES:
|
||||
_copy_file(
|
||||
os.path.join(source_app, *name.split('/')),
|
||||
os.path.join(app_root, *name.split('/')),
|
||||
f'worker authority {name}',
|
||||
)
|
||||
_copy_tree(dependencies_root, os.path.join(app_root, 'dependencies'), 'worker dependencies')
|
||||
policy_relative = 'app/trufflehog-custom-detectors.yaml'
|
||||
_copy_file(
|
||||
detector_policy_source,
|
||||
os.path.join(staging, *policy_relative.split('/')),
|
||||
'detector policy',
|
||||
)
|
||||
|
||||
executable_name = 'trufflehog.exe' if platform_tag.startswith('windows-') else 'trufflehog'
|
||||
trufflehog_relative = f'bin/{executable_name}'
|
||||
_copy_file(
|
||||
trufflehog_source, os.path.join(staging, *trufflehog_relative.split('/')),
|
||||
'TruffleHog executable',
|
||||
)
|
||||
if not platform_tag.startswith('windows-'):
|
||||
os.chmod(os.path.join(staging, *trufflehog_relative.split('/')), 0o755)
|
||||
|
||||
git_root_relative = 'runtime/git'
|
||||
_copy_tree(git_source_root, os.path.join(staging, 'runtime', 'git'), 'Git runtime')
|
||||
git_executable = str(git_executable).replace('\\', '/').strip('/')
|
||||
git_relative = f'{git_root_relative}/{git_executable}'
|
||||
_regular_file(os.path.join(staging, *git_relative.split('/')), 'Git executable')
|
||||
|
||||
python_root_relative = None
|
||||
if platform_tag.startswith('windows-'):
|
||||
if not python_source_root:
|
||||
raise WorkerPackageBuildError('Windows package requires bundled Python')
|
||||
python_root_relative = 'runtime/python'
|
||||
_copy_tree(
|
||||
python_source_root, os.path.join(staging, 'runtime', 'python'),
|
||||
'Python runtime',
|
||||
)
|
||||
_regular_file(
|
||||
os.path.join(staging, 'runtime', 'python', 'python.exe'),
|
||||
'Python executable',
|
||||
)
|
||||
_windows_support_files(staging)
|
||||
elif python_source_root:
|
||||
raise WorkerPackageBuildError('Linux package must use image Python')
|
||||
else:
|
||||
_linux_support_files(staging)
|
||||
|
||||
if build_inputs_path:
|
||||
_copy_file(
|
||||
build_inputs_path, os.path.join(staging, 'worker-build-inputs.json'),
|
||||
'worker build inputs',
|
||||
)
|
||||
_copy_file(
|
||||
operator_readme_source, os.path.join(staging, 'README_RU.md'),
|
||||
'Russian worker operator guide',
|
||||
)
|
||||
for source in operator_cheatsheet_sources:
|
||||
name = os.path.basename(os.fspath(source))
|
||||
if not name.startswith('remote-worker-cheatsheet-') or not name.endswith('-ru.md'):
|
||||
raise WorkerPackageBuildError('worker operator cheatsheet name is invalid')
|
||||
_copy_file(source, os.path.join(staging, name), 'worker operator cheatsheet')
|
||||
manifest = build_worker_package_manifest(
|
||||
staging,
|
||||
trufflehog_path=trufflehog_relative,
|
||||
git_path=git_relative,
|
||||
detector_policy_path=policy_relative,
|
||||
git_root=git_root_relative,
|
||||
python_root=python_root_relative,
|
||||
capabilities=[
|
||||
{
|
||||
'source': source,
|
||||
'platform': platform,
|
||||
'planning_kind': planning_kind,
|
||||
}
|
||||
for source, platform, planning_kind in capabilities
|
||||
],
|
||||
platform_tag=platform_tag,
|
||||
)
|
||||
manifest_path = write_worker_package_manifest(
|
||||
os.path.join(staging, 'worker-package.json'), manifest,
|
||||
)
|
||||
if platform_tag.startswith('windows-'):
|
||||
harden_private_tree(staging)
|
||||
verify_worker_package(manifest_path)
|
||||
os.replace(staging, package_root)
|
||||
staging = None
|
||||
return manifest
|
||||
finally:
|
||||
if staging is not None:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
|
||||
def _download(url, destination, expected_sha256, expected_bytes):
|
||||
if os.path.exists(destination):
|
||||
if os.path.getsize(destination) != expected_bytes or sha256_file(destination) != expected_sha256:
|
||||
raise WorkerPackageBuildError('cached public build input does not match its pin')
|
||||
return destination
|
||||
partial = destination + '.partial'
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=120) as response, open(partial, 'xb') as output:
|
||||
while block := response.read(1024 * 1024):
|
||||
digest.update(block)
|
||||
size += len(block)
|
||||
output.write(block)
|
||||
if size != expected_bytes or digest.hexdigest() != expected_sha256:
|
||||
raise WorkerPackageBuildError('downloaded public build input does not match its pin')
|
||||
os.replace(partial, destination)
|
||||
finally:
|
||||
if os.path.exists(partial):
|
||||
os.unlink(partial)
|
||||
return destination
|
||||
|
||||
|
||||
def _safe_unzip(archive_path, destination):
|
||||
os.makedirs(destination, exist_ok=False)
|
||||
root = os.path.abspath(destination)
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
for item in archive.infolist():
|
||||
name = item.filename.replace('\\', '/')
|
||||
parts = [part for part in name.split('/') if part]
|
||||
if not parts or name.startswith('/') or any(part in ('.', '..') for part in parts):
|
||||
raise WorkerPackageBuildError('ZIP build input contains an unsafe path')
|
||||
mode = item.external_attr >> 16
|
||||
if stat.S_ISLNK(mode):
|
||||
raise WorkerPackageBuildError('ZIP build input contains a link')
|
||||
target = os.path.abspath(os.path.join(root, *parts))
|
||||
if os.path.commonpath((root, target)) != root:
|
||||
raise WorkerPackageBuildError('ZIP build input escapes its destination')
|
||||
if item.is_dir():
|
||||
os.makedirs(target, exist_ok=True)
|
||||
continue
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
with archive.open(item) as source, open(target, 'xb') as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
|
||||
|
||||
def _extract_trufflehog(archive_path, destination):
|
||||
with tarfile.open(archive_path, 'r:gz') as archive:
|
||||
matches = [item for item in archive.getmembers() if item.name == 'trufflehog.exe']
|
||||
if len(matches) != 1 or not matches[0].isfile():
|
||||
raise WorkerPackageBuildError('TruffleHog archive executable is unavailable')
|
||||
with archive.extractfile(matches[0]) as source, open(destination, 'xb') as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
|
||||
|
||||
def _deterministic_zip(root, destination):
|
||||
if os.path.lexists(destination):
|
||||
raise WorkerPackageBuildError('worker archive destination already exists')
|
||||
root = os.path.abspath(root)
|
||||
with zipfile.ZipFile(destination, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
||||
for current, directories, names in os.walk(root, followlinks=False):
|
||||
directories.sort()
|
||||
names.sort()
|
||||
for name in names:
|
||||
path = _regular_file(os.path.join(current, name), 'worker archive file')
|
||||
relative = os.path.relpath(path, root).replace(os.sep, '/')
|
||||
item = zipfile.ZipInfo(relative, (1980, 1, 1, 0, 0, 0))
|
||||
item.compress_type = zipfile.ZIP_DEFLATED
|
||||
item.external_attr = 0o100600 << 16
|
||||
with open(path, 'rb') as source:
|
||||
archive.writestr(item, source.read())
|
||||
return destination
|
||||
|
||||
|
||||
def _normalize_windows_dependency_artifacts(dependencies):
|
||||
bin_root = os.path.join(dependencies, 'bin')
|
||||
normalized = set()
|
||||
if os.path.isdir(bin_root):
|
||||
for name in sorted(os.listdir(bin_root)):
|
||||
if not name.lower().endswith('.exe'):
|
||||
continue
|
||||
path = _regular_file(
|
||||
os.path.join(bin_root, name), 'Windows dependency launcher',
|
||||
)
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
entries = archive.infolist()
|
||||
central_offset = archive.start_dir
|
||||
if not entries:
|
||||
raise WorkerPackageBuildError(
|
||||
'Windows dependency launcher has no embedded ZIP entries'
|
||||
)
|
||||
payload = bytearray(open(path, 'rb').read())
|
||||
for entry in entries:
|
||||
offset = entry.header_offset
|
||||
if payload[offset:offset + 4] != b'PK\x03\x04':
|
||||
raise WorkerPackageBuildError(
|
||||
'Windows dependency launcher local header is invalid'
|
||||
)
|
||||
payload[offset + 10:offset + 14] = b'\x00\x00\x21\x00'
|
||||
offset = central_offset
|
||||
for _entry in entries:
|
||||
if payload[offset:offset + 4] != b'PK\x01\x02':
|
||||
raise WorkerPackageBuildError(
|
||||
'Windows dependency launcher central header is invalid'
|
||||
)
|
||||
payload[offset + 12:offset + 16] = b'\x00\x00\x21\x00'
|
||||
name_bytes, extra_bytes, comment_bytes = struct.unpack_from(
|
||||
'<HHH', payload, offset + 28,
|
||||
)
|
||||
offset += 46 + name_bytes + extra_bytes + comment_bytes
|
||||
with open(path, 'wb') as handle:
|
||||
handle.write(payload)
|
||||
normalized.add(name)
|
||||
|
||||
referenced = set()
|
||||
for current, _directories, names in os.walk(dependencies):
|
||||
if os.path.basename(current).lower().endswith('.dist-info') and 'RECORD' in names:
|
||||
record = os.path.join(current, 'RECORD')
|
||||
with open(record, 'r', encoding='utf-8', newline='') as handle:
|
||||
rows = list(csv.reader(handle))
|
||||
changed = False
|
||||
for row in rows:
|
||||
if len(row) != 3:
|
||||
raise WorkerPackageBuildError('Windows dependency RECORD is invalid')
|
||||
relative = row[0].replace('\\', '/')
|
||||
if not relative.startswith('../../bin/'):
|
||||
continue
|
||||
name = relative.rsplit('/', 1)[-1]
|
||||
path = _regular_file(
|
||||
os.path.join(bin_root, name), 'Windows dependency RECORD launcher',
|
||||
)
|
||||
digest = base64.urlsafe_b64encode(
|
||||
bytes.fromhex(sha256_file(path))
|
||||
).decode('ascii').rstrip('=')
|
||||
row[1] = 'sha256=' + digest
|
||||
row[2] = str(os.path.getsize(path))
|
||||
referenced.add(name)
|
||||
changed = True
|
||||
if changed:
|
||||
with open(record, 'w', encoding='utf-8', newline='') as handle:
|
||||
csv.writer(handle, lineterminator='\r\n').writerows(rows)
|
||||
if referenced != normalized:
|
||||
raise WorkerPackageBuildError(
|
||||
'Windows dependency launchers and RECORD entries do not match'
|
||||
)
|
||||
|
||||
|
||||
def build_windows_portable(project_root, output_root, archive_path, cache_root):
|
||||
project_root = os.path.abspath(project_root)
|
||||
pins_path = os.path.join(project_root, 'docker', 'worker-package-pins.json')
|
||||
with open(pins_path, 'r', encoding='utf-8') as handle:
|
||||
pins = json.load(handle)['windows']['x86_64']
|
||||
os.makedirs(cache_root, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='truf-worker-windows-') as temporary:
|
||||
extracted = {}
|
||||
for name in ('python', 'git', 'trufflehog'):
|
||||
pin = pins[name]
|
||||
suffix = '.tar.gz' if name == 'trufflehog' else '.zip'
|
||||
archive = _download(
|
||||
pin['url'], os.path.join(cache_root, name + suffix),
|
||||
pin['archive_sha256'], int(pin['archive_bytes']),
|
||||
)
|
||||
if name == 'trufflehog':
|
||||
extracted[name] = os.path.join(temporary, 'trufflehog.exe')
|
||||
_extract_trufflehog(archive, extracted[name])
|
||||
else:
|
||||
extracted[name] = os.path.join(temporary, name)
|
||||
_safe_unzip(archive, extracted[name])
|
||||
dependencies = os.path.join(temporary, 'dependencies')
|
||||
subprocess.run([
|
||||
sys.executable, '-m', 'pip', '--isolated', 'install',
|
||||
'--require-hashes', '--only-binary=:all:', '--no-compile', '--no-deps',
|
||||
'--disable-pip-version-check', '--platform=win_amd64',
|
||||
'--python-version=3.12', '--implementation=cp', '--abi=cp312',
|
||||
'--target', dependencies,
|
||||
'-r', os.path.join(project_root, 'docker', 'requirements-worker.lock'),
|
||||
], check=True)
|
||||
_normalize_windows_dependency_artifacts(dependencies)
|
||||
manifest = assemble_worker_package(
|
||||
output_root,
|
||||
source_app=os.path.join(project_root, 'app'),
|
||||
dependencies_root=dependencies,
|
||||
detector_policy_source=os.path.join(project_root, 'app', 'trufflehog-custom-detectors.yaml'),
|
||||
trufflehog_source=extracted['trufflehog'],
|
||||
git_source_root=extracted['git'],
|
||||
git_executable='cmd/git.exe',
|
||||
python_source_root=extracted['python'],
|
||||
build_inputs_path=pins_path,
|
||||
operator_readme_source=os.path.join(
|
||||
project_root, 'docs', 'remote-worker-quickstart-ru.md',
|
||||
),
|
||||
operator_cheatsheet_sources=[
|
||||
os.path.join(project_root, 'docs', f'remote-worker-cheatsheet-{name}-ru.md')
|
||||
for name in ('windows', 'linux', 'docker')
|
||||
],
|
||||
platform_tag='windows-x86_64',
|
||||
)
|
||||
_deterministic_zip(output_root, archive_path)
|
||||
release = {
|
||||
'schema': 1,
|
||||
'platform_tag': 'windows-x86_64',
|
||||
'archive': os.path.basename(archive_path),
|
||||
'archive_bytes': os.path.getsize(archive_path),
|
||||
'archive_sha256': sha256_file(archive_path),
|
||||
'package_manifest_sha256': worker_package_manifest_sha256(manifest),
|
||||
'build_inputs_sha256': sha256_file(pins_path),
|
||||
}
|
||||
release_path = archive_path + '.json'
|
||||
if os.path.lexists(release_path):
|
||||
raise WorkerPackageBuildError('worker release metadata destination already exists')
|
||||
with open(release_path, 'x', encoding='ascii', newline='\n') as handle:
|
||||
json.dump(release, handle, ensure_ascii=True, sort_keys=True, separators=(',', ':'))
|
||||
handle.write('\n')
|
||||
harden_private_tree(release_path)
|
||||
return release
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
||||
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||
windows = subparsers.add_parser('windows', allow_abbrev=False)
|
||||
windows.add_argument('--project-root', default=PROJECT_DIR)
|
||||
windows.add_argument('--output', required=True)
|
||||
windows.add_argument('--archive', required=True)
|
||||
windows.add_argument('--cache', required=True)
|
||||
assemble = subparsers.add_parser('assemble', allow_abbrev=False)
|
||||
assemble.add_argument('--output', required=True)
|
||||
assemble.add_argument('--source-app', required=True)
|
||||
assemble.add_argument('--dependencies', required=True)
|
||||
assemble.add_argument('--detector-policy', required=True)
|
||||
assemble.add_argument('--trufflehog', required=True)
|
||||
assemble.add_argument('--git-root', required=True)
|
||||
assemble.add_argument('--git-executable', required=True)
|
||||
assemble.add_argument('--platform-tag', required=True)
|
||||
assemble.add_argument('--python-root')
|
||||
assemble.add_argument('--build-inputs')
|
||||
assemble.add_argument('--operator-readme', required=True)
|
||||
assemble.add_argument('--operator-cheatsheet', action='append', default=[])
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
if args.command == 'windows':
|
||||
release = build_windows_portable(
|
||||
args.project_root, args.output, args.archive, args.cache,
|
||||
)
|
||||
print(json.dumps(release, ensure_ascii=True, sort_keys=True))
|
||||
elif args.command == 'assemble':
|
||||
manifest = assemble_worker_package(
|
||||
args.output,
|
||||
source_app=args.source_app,
|
||||
dependencies_root=args.dependencies,
|
||||
detector_policy_source=args.detector_policy,
|
||||
trufflehog_source=args.trufflehog,
|
||||
git_source_root=args.git_root,
|
||||
git_executable=args.git_executable,
|
||||
platform_tag=args.platform_tag,
|
||||
python_source_root=args.python_root,
|
||||
build_inputs_path=args.build_inputs,
|
||||
operator_readme_source=args.operator_readme,
|
||||
operator_cheatsheet_sources=args.operator_cheatsheet,
|
||||
)
|
||||
print(worker_package_manifest_sha256(manifest))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user