538 lines
24 KiB
Python
538 lines
24 KiB
Python
"""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()
|