cloud-build/cloud_build/cloud_build.py

887 lines
30 KiB
Python
Raw Normal View History

2020-04-06 23:29:37 +03:00
#!/usr/bin/python3
2021-07-29 13:57:40 +03:00
from typing import Dict, List, Set, Tuple, Union, Optional
2020-04-06 23:29:37 +03:00
from pathlib import Path
2020-04-06 23:29:37 +03:00
import contextlib
import datetime
import fcntl
import logging
import os
import re
import shutil
2021-07-29 13:57:40 +03:00
import string
2020-04-06 23:29:37 +03:00
import subprocess
import time
2020-04-06 23:29:37 +03:00
import yaml
import cloud_build.image_tests
2021-08-08 20:35:21 +03:00
import cloud_build.rename
2020-04-06 23:29:37 +03:00
PROG = 'cloud-build'
# types
PathLike = Union[Path, str]
2020-04-06 23:29:37 +03:00
2020-04-20 14:29:27 +03:00
class Error(Exception):
pass
class BuildError(Error):
def __init__(self, target: str, arch: str):
self.target = target
self.arch = arch
2021-03-19 18:25:39 +03:00
def __str__(self) -> str:
return f'Fail building of {self.target} {self.arch}'
class MultipleBuildErrors(Error):
def __init__(self, build_errors: List[BuildError]):
self.build_errors = build_errors
2021-03-19 18:25:39 +03:00
def __str__(self) -> str:
s = 'Fail building of the following targets:\n'
s += '\n'.join(f' {be.target} {be.arch}' for be in self.build_errors)
return s
2020-04-06 23:29:37 +03:00
class CB:
"""class for building cloud images"""
2020-04-20 17:27:33 +03:00
def __init__(
self,
2021-03-19 18:25:39 +03:00
config: str,
*,
2021-03-19 18:25:39 +03:00
data_dir: Optional[PathLike] = None,
tasks: Optional[dict[str, List[str]]] = None,
built_images_dir: Optional[PathLike] = None,
config_override: Optional[Dict] = None,
2020-04-20 17:27:33 +03:00
) -> None:
self.initialized = False
2020-04-18 03:01:07 +03:00
self._save_cwd = os.getcwd()
self.parse_config(config, config_override)
if config_override \
and (
'mkimage_profiles_branch' in config_override
or 'mkimage_profiles_git' in config_override
):
self.force_recreate_mp = True
else:
self.force_recreate_mp = False
2020-04-25 01:04:10 +03:00
if tasks is None:
self.tasks = {}
else:
self.tasks = tasks
2020-04-06 23:29:37 +03:00
if not data_dir:
data_dir = (Path(self.expand_path(os.getenv('XDG_DATA_HOME',
'~/.local/share')))
/ f'{PROG}')
else:
2021-06-16 01:35:48 +03:00
data_dir = Path(data_dir).absolute()
2020-04-06 23:29:37 +03:00
self.data_dir = data_dir
self.checksum_command = 'sha256sum'
if built_images_dir:
2021-07-29 13:57:40 +03:00
self._images_dir = Path(built_images_dir).absolute()
self.no_build = True
else:
2021-07-29 13:57:40 +03:00
self._images_dir = data_dir / 'images'
self.no_build = False
2020-04-06 23:29:37 +03:00
self.work_dir = data_dir / 'work'
self.out_dir = data_dir / 'out'
self.service_default_state = 'enabled'
self.created_scripts: List[Path] = []
self._build_errors: List[BuildError] = []
2020-04-06 23:29:37 +03:00
self.ensure_dirs()
logging.basicConfig(
filename=f'{data_dir}/{PROG}.log',
format='%(levelname)s:%(asctime)s - %(message)s',
)
self.log = logging.getLogger(PROG)
self.log.setLevel(self.log_level)
2020-04-20 14:29:27 +03:00
self.ensure_run_once()
2020-04-06 23:29:37 +03:00
self.info(f'Start {PROG}')
self.initialized = True
2020-04-06 23:29:37 +03:00
def __del__(self) -> None:
if not self.initialized:
2020-04-18 02:27:41 +03:00
if getattr(self, 'lock_file', False):
self.lock_file.close()
return
2022-05-31 01:26:09 +03:00
# check directory exists for test: work dir deleted to early
if (self.work_dir / 'mkimage-profiles' / '.git').exists():
os.chdir(self.work_dir / 'mkimage-profiles')
subprocess.run(['git', 'reset', '--hard'])
subprocess.run(['git', 'clean', '-fdx'])
2020-04-18 03:01:07 +03:00
os.chdir(self._save_cwd)
2020-04-20 23:19:24 +03:00
try:
self.info(f'Finish {PROG}')
except FileNotFoundError:
pass
2020-04-18 02:27:41 +03:00
self.lock_file.close()
2020-04-06 23:29:37 +03:00
2021-07-29 13:57:40 +03:00
@property
def _remote_formaters(self) -> Set[str]:
return {
key
for tup in string.Formatter().parse(self._remote)
if (key := tup[1]) is not None
}
@property
def is_remote_arch(self) -> bool:
return 'arch' in self._remote_formaters
@property
def is_remote_branch(self) -> bool:
return 'branch' in self._remote_formaters
def images_dirs_remotes_list(self) -> List[Tuple[Path, str]]:
images_dirs_list = []
images_dir = self._images_dir
if self.is_remote_branch:
for branch in self.branches:
if self.is_remote_arch:
for arch in self.arches_by_branch(branch):
remote = self._remote.format(branch=branch, arch=arch)
pair = (images_dir / branch / arch, remote)
images_dirs_list.append(pair)
else:
remote = self._remote.format(branch=branch)
images_dirs_list.append((images_dir / branch, remote))
else:
if self.is_remote_arch:
for arch in self.all_arches:
remote = self._remote.format(arch=arch)
images_dirs_list.append((images_dir / arch, remote))
else:
images_dirs_list.append((images_dir, self._remote))
return images_dirs_list
def images_dirs_list(self) -> List[Path]:
return [pair[0] for pair in self.images_dirs_remotes_list()]
def images_dir(self, branch: str, arch: str) -> Path:
images_dir = self._images_dir
if self.is_remote_branch:
images_dir = images_dir / branch
if self.is_remote_arch:
images_dir = images_dir / arch
return images_dir
def expand_path(self, path: PathLike):
result = os.path.expanduser(os.path.expandvars(path))
if isinstance(path, Path):
return Path(result)
else:
return result
2021-03-19 18:25:39 +03:00
def ensure_run_once(self) -> None:
2020-04-06 23:29:37 +03:00
self.lock_file = open(self.data_dir / f'{PROG}.lock', 'w')
try:
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError: # already locked
2020-04-20 19:56:19 +03:00
dd = self.data_dir
msg = f'Program {PROG} already running in `{dd}` directory'
self.error(msg)
2020-04-06 23:29:37 +03:00
@contextlib.contextmanager
def pushd(self, new_dir):
previous_dir = os.getcwd()
self.debug(f'Pushd to {new_dir}')
os.chdir(new_dir)
yield
self.debug(f'Popd from {new_dir}')
os.chdir(previous_dir)
def parse_config(
self,
config: str,
override: Optional[Dict] = None
) -> None:
if override is None:
override = {}
try:
with open(config) as f:
cfg = yaml.safe_load(f)
2020-04-20 19:56:19 +03:00
except OSError as e:
msg = f'Could not read config file `{e.filename}`: {e.strerror}'
raise Error(msg)
2020-04-06 23:29:37 +03:00
def get_overrided(key, default=None):
return override.get(key, cfg.get(key, default))
2022-05-30 17:15:03 +03:00
2022-02-09 01:19:03 +03:00
def lazy_get_raises(key):
if key in override:
return override[key]
else:
return cfg[key]
self.mkimage_profiles_git = self.expand_path(
get_overrided('mkimage_profiles_git', '')
2020-04-06 23:29:37 +03:00
)
self.mkimage_profiles_branch = get_overrided('mkimage_profiles_branch')
2020-04-06 23:29:37 +03:00
self.log_level = getattr(logging, cfg.get('log_level', 'INFO').upper())
self._repository_url = cfg.get('repository_url',
2020-04-25 00:07:45 +03:00
'copy:///space/ALT/{branch}')
2020-04-06 23:29:37 +03:00
2022-03-04 17:26:40 +03:00
self._image_repo = cfg.get('image_repo')
2022-05-30 17:15:03 +03:00
self.patch_mp_prog = get_overrided('patch_mp_prog')
if (patch_mp_prog := self.patch_mp_prog) is not None:
self.patch_mp_prog = self.expand_path(
Path(patch_mp_prog)
).absolute().as_posix()
self.try_build_all = cfg.get('try_build_all', False)
2020-05-06 16:03:06 +03:00
self.no_delete = cfg.get('no_delete', True)
2020-04-06 23:29:37 +03:00
self.bad_arches = cfg.get('bad_arches', [])
self.external_files = cfg.get('external_files')
if self.external_files:
self.external_files = self.expand_path(Path(self.external_files))
2020-04-06 23:29:37 +03:00
rebuild_after = override.get(
'rebuild_after',
cfg.get('rebuild_after', {'days': 1}),
)
try:
self.rebuild_after = datetime.timedelta(**rebuild_after)
except TypeError as e:
m = re.match(r"'([^']+)'", str(e))
if m:
arg = m.groups()[0]
raise Error(f'Invalid key `{arg}` passed to rebuild_after')
else:
raise
2020-04-06 23:29:37 +03:00
self._packages = cfg.get('packages', {})
self._services = cfg.get('services', {})
self._scripts = cfg.get('scripts', {})
self._after_sync_commands = cfg.get('after_sync_commands', [])
2022-02-08 03:07:55 +03:00
self.key = override.get('key', cfg.get('key'))
if isinstance(self.key, int):
self.key = '{:X}'.format(self.key)
2020-04-06 23:29:37 +03:00
try:
2022-02-09 01:19:03 +03:00
self._remote = self.expand_path(lazy_get_raises('remote'))
self._images = lazy_get_raises('images')
self._branches = lazy_get_raises('branches')
2020-04-06 23:29:37 +03:00
for _, branch in self._branches.items():
branch['arches'] = {k: {} if v is None else v
for k, v in branch['arches'].items()}
except KeyError as e:
msg = f'Required parameter {e} does not set in config'
2020-04-20 19:56:19 +03:00
raise Error(msg)
2020-04-06 23:29:37 +03:00
def info(self, msg: str) -> None:
self.log.info(msg)
def debug(self, msg: str) -> None:
self.log.debug(msg)
def error(self, arg: Union[str, Error]) -> None:
if isinstance(arg, Error):
err = arg
else:
err = Error(arg)
self.log.error(err)
raise err
2020-04-06 23:29:37 +03:00
2021-07-29 13:57:40 +03:00
def remote(self, branch: str, arch: str) -> str:
return self._remote.format(branch=branch, arch=arch)
2020-04-06 23:29:37 +03:00
def repository_url(self, branch: str, arch: str) -> str:
url = self._branches[branch]['arches'][arch].get('repository_url')
if url is None:
url = self._branches[branch].get('repository_url',
self._repository_url)
return url.format(branch=branch, arch=arch)
2022-03-04 17:26:40 +03:00
def image_repo(self, branch: str, arch: str) -> str:
url = self._branches[branch]['arches'][arch].get('image_repo')
if url is None:
url = self._branches[branch].get('image_repo',
self._image_repo)
if url is not None:
url = url.format(branch=branch, arch=arch)
return url
2020-04-06 23:29:37 +03:00
def call(
self,
cmd: List[str],
*,
stdout_to_file: str = '',
fail_on_error: bool = True,
) -> None:
def maybe_fail(string: str, rc: int) -> None:
if fail_on_error:
if rc != 0:
msg = 'Command `{}` failed with {} return code'.format(
string,
rc,
)
self.error(msg)
# just_print = True
just_print = False
string = ' '.join(cmd)
self.debug(f'Call `{string}`')
if just_print:
print(string)
else:
if stdout_to_file:
# TODO rewrite using subprocess.run
2020-04-06 23:29:37 +03:00
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
rc = p.wait()
maybe_fail(string, rc)
# TODO rewrite by passing f as stdout value
2020-04-06 23:29:37 +03:00
with open(stdout_to_file, 'w') as f:
if p.stdout:
f.write(p.stdout.read().decode())
2020-04-18 03:06:44 +03:00
if p.stdout is not None:
p.stdout.close()
2020-04-06 23:29:37 +03:00
else:
# TODO rewrite using subprocess.run
2020-04-06 23:29:37 +03:00
rc = subprocess.call(cmd)
maybe_fail(string, rc)
def ensure_dirs(self) -> None:
for attr in dir(self):
if attr.endswith('_dir'):
value = getattr(self, attr)
if isinstance(value, str) or isinstance(value, os.PathLike):
os.makedirs(value, exist_ok=True)
2021-07-29 13:57:40 +03:00
for images_dir in self.images_dirs_list():
os.makedirs(images_dir, exist_ok=True)
2020-04-06 23:29:37 +03:00
def generate_apt_files(self) -> None:
apt_dir = self.work_dir / 'apt'
os.makedirs(apt_dir, exist_ok=True)
for branch in self.branches:
for arch in self.arches_by_branch(branch):
repo = self.repository_url(branch, arch)
with open(f'{apt_dir}/apt.conf.{branch}.{arch}', 'w') as f:
apt_conf = f'''
Dir::Etc::main "/dev/null";
Dir::Etc::parts "/var/empty";
Dir::Etc::SourceList "{apt_dir}/sources.list.{branch}.{arch}";
Dir::Etc::SourceParts "/var/empty";
Dir::Etc::preferences "/dev/null";
Dir::Etc::preferencesparts "/var/empty";
'''.lstrip()
2020-04-06 23:29:37 +03:00
f.write(apt_conf)
with open(f'{apt_dir}/sources.list.{branch}.{arch}', 'w') as f:
sources_list = f'rpm {repo} {arch} classic\n'
2022-05-30 02:22:18 +03:00
if arch == 'x86_64':
sources_list += f'rpm {repo} {arch}-i586 classic\n'
2020-04-06 23:29:37 +03:00
if arch not in self.bad_arches:
sources_list += f'rpm {repo} noarch classic\n'
2020-04-25 01:04:10 +03:00
for task in self.tasks.get(branch.lower(), []):
tr = 'http://git.altlinux.org'
sources_list += f'rpm {tr} repo/{task}/{arch} task\n'
2020-04-06 23:29:37 +03:00
f.write(sources_list)
def escape_branch(self, branch: str) -> str:
return re.sub(r'\.', '_', branch)
2022-05-30 17:15:03 +03:00
def patch_mp(self):
if (patch_mp_prog := self.patch_mp_prog) is not None:
self.call([patch_mp_prog])
def ensure_mkimage_profiles(self, force_recreate=False) -> None:
2020-04-06 23:29:37 +03:00
"""Checks that mkimage-profiles exists or clones it"""
def add_recipe(variable: str, value: str) -> str:
return f'\n\t@$(call add,{variable},{value})'
url = self.mkimage_profiles_git
if url == '':
url = (
'git://'
+ 'git.altlinux.org/'
+ 'people/antohami/packages/mkimage-profiles.git'
2020-04-06 23:29:37 +03:00
)
os.chdir(self.work_dir)
if force_recreate and os.path.isdir('mkimage-profiles'):
shutil.rmtree('mkimage-profiles')
2020-04-06 23:29:37 +03:00
if os.path.isdir('mkimage-profiles'):
2021-08-13 01:43:57 +03:00
with self.pushd('mkimage-profiles'):
self.info('Updating mkimage-profiles')
self.call(['git', 'pull', '--ff-only'], fail_on_error=True)
2020-04-06 23:29:37 +03:00
else:
self.info('Downloading mkimage-profiles')
git_clone = ['git', 'clone', url, 'mkimage-profiles']
if branch := self.mkimage_profiles_branch:
git_clone.extend(['--branch', branch])
self.call(git_clone)
2020-04-06 23:29:37 +03:00
# create file with proper brandings
with self.pushd('mkimage-profiles'):
2022-05-30 17:15:03 +03:00
self.patch_mp()
2020-04-06 23:29:37 +03:00
with open(f'conf.d/{PROG}.mk', 'w') as f:
for image in self.images:
target = self.target_by_image(image)
for branch in self.branches:
ebranch = self.escape_branch(branch)
prerequisites = [target]
prerequisites.extend(
self.prerequisites_by_branch(branch)
)
prerequisites.extend(
self.prerequisites_by_image(image)
)
prerequisites_s = ' '.join(prerequisites)
2022-01-28 02:27:00 +03:00
recipes = []
2020-04-06 23:29:37 +03:00
for package in self.packages(image, branch):
recipes.append(
add_recipe(
'BASE_PACKAGES',
package))
for service in self.enabled_services(image, branch):
recipes.append(
add_recipe(
'DEFAULT_SERVICES_ENABLE',
service))
for service in self.disabled_services(image, branch):
recipes.append(
add_recipe(
'DEFAULT_SERVICES_DISABLE',
service))
recipes_s = ''.join(recipes)
rule = f'''
{target}_{ebranch}: {prerequisites_s}; @:{recipes_s}
'''.strip()
print(rule, file=f)
self.generate_apt_files()
@property
def branches(self) -> List[str]:
return list(self._branches.keys())
def arches_by_branch(self, branch: str) -> List[str]:
return list(self._branches[branch]['arches'].keys())
2021-07-29 13:57:40 +03:00
@property
def all_arches(self) -> List[str]:
arches: Set[str] = set()
for branch in self.branches:
arches |= set(self.arches_by_branch(branch))
return list(arches)
2020-04-06 23:29:37 +03:00
def prerequisites_by_branch(self, branch: str) -> List[str]:
return self._branches[branch].get('prerequisites', [])
@property
def images(self) -> List[str]:
return list(self._images.keys())
def kinds_by_image(self, image: str) -> List[str]:
return self._images[image]['kinds']
2020-07-03 15:24:19 +03:00
def convert_size(self, size: str) -> Optional[str]:
result = None
multiplier = {
'': 1,
'k': 2 ** 10,
'm': 2 ** 20,
'g': 2 ** 30,
}
match = re.match(
r'^(?P<num> \d+(:?.\d+)? ) (?P<suff> [kmg] )?$',
size,
re.IGNORECASE | re.VERBOSE,
)
if not match:
self.error('Bad size format')
else:
num = float(match.group('num'))
suff = match.group('suff')
if suff is None:
suff = ''
mul = multiplier[str.lower(suff)]
result = str(round(num * mul))
return result
def size_by_image(self, image: str) -> Optional[str]:
size = self._images[image].get('size')
if size is not None:
size = self.convert_size(str(size))
return size
2020-04-06 23:29:37 +03:00
def target_by_image(self, image: str) -> str:
return self._images[image]['target']
def prerequisites_by_image(self, image: str) -> List[str]:
return self._images[image].get('prerequisites', [])
def tests_by_image(self, image: str) -> List[Dict]:
return self._images[image].get('tests', [])
def scripts_by_image(self, image: str) -> Dict[str, str]:
scripts = {}
for name, value in self._scripts.items():
number = value.get('number')
if (
value.get('global', False)
and name not in self._images[image].get('no_scripts', [])
or name in self._images[image].get('scripts', [])
):
if number is not None:
if isinstance(number, int):
number = f'{number:02}'
name = f'{number}-{name}'
scripts[name] = value['contents']
return scripts
def skip_arch(self, image: str, arch: str) -> bool:
return arch in self._images[image].get('exclude_arches', [])
2020-04-17 13:48:00 +03:00
def skip_branch(self, image: str, branch: str) -> bool:
return branch in self._images[image].get('exclude_branches', [])
2020-04-06 23:29:37 +03:00
def get_items(
self,
data: Dict,
image: str,
branch: str,
state_re: str = None,
default_state: str = None,
) -> List[str]:
items = []
if state_re is None:
state_re = ''
if default_state is None:
default_state = state_re
for item, constraints in data.items():
2020-05-07 18:23:07 +03:00
if constraints is None:
constraints = {}
2020-04-06 23:29:37 +03:00
if (
image in constraints.get('exclude_images', [])
or branch in constraints.get('exclude_branches', [])
):
continue
# Empty means no constraint: e.g. all images
images = constraints.get('images', [image])
branches = constraints.get('branch', [branch])
state = constraints.get('state', default_state)
if (
image in images
and branch in branches
and re.match(state_re, state)
):
items.append(item)
return items
def branding(self, image: str, branch: str) -> Optional[str]:
2022-05-27 00:58:15 +03:00
if (image_branding := self._images[image].get('branding')) is not None:
if image_branding.lower() == 'none':
return None
else:
return image_branding
2022-05-27 00:58:15 +03:00
return self._branches[branch].get('branding')
2022-05-27 00:58:15 +03:00
2020-04-06 23:29:37 +03:00
def packages(self, image: str, branch: str) -> List[str]:
2021-03-03 17:23:54 +03:00
image_packages = self._images[image].get('packages', [])
return image_packages + self.get_items(self._packages, image, branch)
2020-04-06 23:29:37 +03:00
def enabled_services(self, image: str, branch: str) -> List[str]:
2021-03-03 17:23:54 +03:00
image_services = self._images[image].get('services_enabled', [])
return image_services + self.get_items(
2020-04-06 23:29:37 +03:00
self._services,
image,
branch,
'enabled?',
self.service_default_state,
)
def disabled_services(self, image: str, branch: str) -> List[str]:
2021-03-03 17:23:54 +03:00
image_services = self._images[image].get('services_disabled', [])
return image_services + self.get_items(
2020-04-06 23:29:37 +03:00
self._services,
image,
branch,
'disabled?',
self.service_default_state,
)
def build_failed(self, target, arch):
if self.try_build_all:
self._build_errors.append(BuildError(target, arch))
else:
self.error(BuildError(target, arch))
def should_rebuild(self, tarball):
if not os.path.exists(tarball):
rebuild = True
else:
lived = time.time() - os.path.getmtime(tarball)
delta = datetime.timedelta(seconds=lived)
rebuild = delta > self.rebuild_after
if rebuild:
os.unlink(tarball)
return rebuild
2020-04-06 23:29:37 +03:00
def build_tarball(
self,
target: str,
branding: Optional[str],
2020-04-06 23:29:37 +03:00
branch: str,
arch: str,
2020-07-03 15:24:19 +03:00
kind: str,
size: str = None,
2020-04-25 00:50:07 +03:00
) -> Optional[Path]:
2020-04-06 23:29:37 +03:00
target = f'{target}_{self.escape_branch(branch)}'
image = re.sub(r'.*/', '', target)
full_target = f'{target}.{kind}'
tarball_name = f'{image}-{arch}.{kind}'
tarball_path = self.out_dir / tarball_name
2020-05-05 21:20:50 +03:00
result: Optional[Path] = tarball_path
2020-04-06 23:29:37 +03:00
apt_dir = self.work_dir / 'apt'
with self.pushd(self.work_dir / 'mkimage-profiles'):
if not self.should_rebuild(tarball_path):
2020-04-06 23:29:37 +03:00
self.info(f'Skip building of {full_target} {arch}')
else:
2022-03-04 17:26:40 +03:00
image_repo = self.image_repo(branch, arch)
2020-04-06 23:29:37 +03:00
cmd = [
'make',
f'APTCONF={apt_dir}/apt.conf.{branch}.{arch}',
f'ARCH={arch}',
2022-01-28 02:24:35 +03:00
f'BRANCH={branch}',
2020-04-06 23:29:37 +03:00
f'IMAGE_OUTDIR={self.out_dir}',
f'IMAGE_OUTFILE={tarball_name}',
2020-04-06 23:29:37 +03:00
]
if branding is not None:
cmd.append(f'BRANDING={branding}')
2022-03-04 17:26:40 +03:00
if image_repo is not None:
cmd.append(f'REPO={image_repo}')
2020-07-03 15:24:19 +03:00
if size is not None:
cmd.append(f'VM_SIZE={size}')
cmd.append(full_target)
2020-04-06 23:29:37 +03:00
self.info(f'Begin building of {full_target} {arch}')
self.call(cmd, fail_on_error=False)
if os.path.exists(tarball_path):
2020-04-06 23:29:37 +03:00
self.info(f'End building of {full_target} {arch}')
else:
2020-05-05 21:20:50 +03:00
result = None
self.build_failed(full_target, arch)
2020-04-06 23:29:37 +03:00
2020-04-25 00:50:07 +03:00
return result
2020-04-06 23:29:37 +03:00
def image_path(
self,
image: str,
branch: str,
arch: str,
kind: str
) -> Path:
2021-08-08 20:35:21 +03:00
name = f'alt-{branch.lower()}-{image}-{arch}.{kind}'
rename_dict = self._images[image].get('rename', {})
if rename_dict:
name = cloud_build.rename.rename(rename_dict, name)
path = self.images_dir(branch, arch) / name
2020-04-06 23:29:37 +03:00
return path
2021-06-16 01:45:39 +03:00
def copy_image(self, src: Path, dst: Path, *, rewrite=False) -> None:
if rewrite and dst.exists():
os.unlink(dst)
2020-04-06 23:29:37 +03:00
os.link(src, dst)
def clear_images_dir(self):
2021-07-29 13:57:40 +03:00
for images_dir in self.images_dirs_list():
for path in images_dir.iterdir():
2021-07-30 00:38:44 +03:00
if path.is_file():
os.unlink(path)
else:
shutil.rmtree(path)
2020-04-06 23:29:37 +03:00
def remove_old_tarballs(self):
with self.pushd(self.out_dir):
for tb in os.listdir():
lived = time.time() - os.path.getmtime(tb)
delta = datetime.timedelta(seconds=lived)
if delta > self.rebuild_after:
2020-04-06 23:29:37 +03:00
os.unlink(tb)
def ensure_scripts(self, image):
for name in self.created_scripts:
os.unlink(name)
self.created_scripts = []
target_type = re.sub(r'(?:(\w+)/)?.*', r'\1',
self.target_by_image(image))
if not target_type:
target_type = 'distro'
scripts_path = (
self.work_dir
/ 'mkimage-profiles'
/ 'features.in'
/ f'build-{target_type}'
/ 'image-scripts.d'
)
for name, content in self.scripts_by_image(image).items():
script = scripts_path / name
self.created_scripts.append(script)
script.write_text(content)
os.chmod(script, 0o755)
def ensure_build_success(self) -> None:
if self._build_errors:
self.error(MultipleBuildErrors(self._build_errors))
2021-04-10 01:03:47 +03:00
def create_images(self, no_tests: bool = False) -> None:
if self.no_build:
msg = 'Trying to build images when build stage should be skipped'
self.error(msg)
2020-04-06 23:29:37 +03:00
self.clear_images_dir()
self.ensure_mkimage_profiles(self.force_recreate_mp)
2020-04-06 23:29:37 +03:00
for branch in self.branches:
for image in self.images:
2020-04-17 13:48:00 +03:00
if self.skip_branch(image, branch):
continue
2020-04-06 23:29:37 +03:00
self.ensure_scripts(image)
target = self.target_by_image(image)
branding = self.branding(image, branch)
2020-04-06 23:29:37 +03:00
for arch in self.arches_by_branch(branch):
if self.skip_arch(image, arch):
continue
for kind in self.kinds_by_image(image):
2020-07-03 15:24:19 +03:00
size = self.size_by_image(image)
2020-04-06 23:29:37 +03:00
tarball = self.build_tarball(
target, branding, branch, arch, kind, size
2020-04-06 23:29:37 +03:00
)
if tarball is None:
continue
2021-08-08 20:35:21 +03:00
2020-04-06 23:29:37 +03:00
image_path = self.image_path(image, branch, arch, kind)
self.copy_image(tarball, image_path)
2021-04-10 01:03:47 +03:00
if not no_tests:
2020-04-06 23:29:37 +03:00
for test in self.tests_by_image(image):
self.info(f'Test {image} {branch} {arch}')
if not cloud_build.image_tests.test(
image=image_path,
branch=branch,
arch=arch,
**test,
):
self.error(f'Test for {image} failed')
self.ensure_build_success()
2020-04-06 23:29:37 +03:00
self.remove_old_tarballs()
def copy_external_files(self):
2021-07-29 13:57:40 +03:00
if not self.external_files:
return
2020-04-06 23:29:37 +03:00
2021-07-29 13:57:40 +03:00
for branch in os.listdir(self.external_files):
if branch not in self.branches:
self.error(f'Unknown branch {branch} in external_files')
arches = self.arches_by_branch(branch)
for arch in os.listdir(self.external_files / branch):
if arch not in arches:
self.error(f'Unknown arch {arch} in external_files')
with self.pushd(self.external_files / branch / arch):
2020-04-06 23:29:37 +03:00
for image in os.listdir():
2021-07-29 13:57:40 +03:00
msg = f'Copy external file {image} in {branch}/{arch}'
self.info(msg)
2021-06-16 01:45:39 +03:00
self.copy_image(
image,
2021-07-29 13:57:40 +03:00
self.images_dir(branch, arch) / image,
2021-06-16 01:45:39 +03:00
rewrite=True,
)
2020-04-06 23:29:37 +03:00
def sign(self):
2021-04-10 01:03:47 +03:00
if self.key is None:
self.error('Pass key to config file for sign')
2020-05-06 15:24:48 +03:00
2020-04-06 23:29:37 +03:00
sum_file = self.checksum_command.upper()
2021-07-29 13:57:40 +03:00
for images_dir in self.images_dirs_list():
with self.pushd(images_dir):
2020-04-06 23:29:37 +03:00
files = [f
for f in os.listdir()
if not f.startswith(sum_file)]
string = ','.join(files)
cmd = [self.checksum_command] + files
self.info(f'Calculate checksum of {string}')
self.call(cmd, stdout_to_file=sum_file)
shutil.copyfile(sum_file, 'SHA256SUMS')
2020-04-06 23:29:37 +03:00
self.info(f'Sign checksum of {string}')
self.call(['gpg2', '--yes', '-basu', self.key, sum_file])
shutil.copyfile(sum_file + '.asc', 'SHA256SUMS.gpg')
2020-04-06 23:29:37 +03:00
def after_sync_commands(self):
2020-04-06 23:29:37 +03:00
remote = self._remote
colon = remote.find(':')
if colon != -1:
host = remote[:colon]
def cmd(command):
return ['ssh', host, command]
else:
host = remote
def cmd(command):
return [command]
for command in self._after_sync_commands:
self.call(cmd(command))
2020-04-06 23:29:37 +03:00
def sync(self, create_remote_dirs: bool = False) -> None:
2021-07-29 13:57:40 +03:00
for images_dir, remote in self.images_dirs_remotes_list():
if create_remote_dirs:
2020-04-16 16:17:44 +03:00
os.makedirs(remote, exist_ok=True)
2020-04-06 23:29:37 +03:00
cmd = [
'rsync',
2021-07-29 13:57:40 +03:00
f'{images_dir}/',
2024-03-24 21:15:07 +03:00
'-rvl',
2020-04-06 23:29:37 +03:00
remote,
]
2020-05-06 16:03:06 +03:00
if not self.no_delete:
cmd.append('--delete')
2020-04-06 23:29:37 +03:00
self.call(cmd)
self.after_sync_commands()