new dependency generation algorithm, dynamic chroot paths [almost] everywhere
This commit is contained in:
parent
e198097cf1
commit
44261ffccb
7 changed files with 279 additions and 187 deletions
43
chroot.py
43
chroot.py
|
@ -10,10 +10,37 @@ def get_chroot_path(chroot_name, override_basepath: str = None) -> str:
|
||||||
return os.path.join(base_path, chroot_name)
|
return os.path.join(base_path, chroot_name)
|
||||||
|
|
||||||
|
|
||||||
def create_chroot(chroot_name, packages=['base'], pacman_conf='/app/local/etc/pacman.conf', extra_repos={}, chroot_base_path: str = None):
|
def create_chroot(
|
||||||
|
chroot_name,
|
||||||
|
arch='aarch64',
|
||||||
|
packages=['base'],
|
||||||
|
pacman_conf='/app/local/etc/pacman.conf',
|
||||||
|
extra_repos={},
|
||||||
|
chroot_base_path: str = None,
|
||||||
|
):
|
||||||
|
base_chroot = f'base_{arch}'
|
||||||
chroot_path = get_chroot_path(chroot_name, override_basepath=chroot_base_path)
|
chroot_path = get_chroot_path(chroot_name, override_basepath=chroot_base_path)
|
||||||
pacman_conf_target = chroot_path + '/etc/pacman.conf'
|
pacman_conf_target = chroot_path + '/etc/pacman.conf'
|
||||||
|
|
||||||
|
# copy base_chroot instead of creating from scratch every time
|
||||||
|
if not (chroot_base_path or chroot_name == base_chroot):
|
||||||
|
# only install base package in base_chroot
|
||||||
|
base_chroot_path = create_chroot(base_chroot, arch=arch)
|
||||||
|
logging.info(f'Copying {base_chroot} chroot to {chroot_name}')
|
||||||
|
result = subprocess.run([
|
||||||
|
'rsync',
|
||||||
|
'-a',
|
||||||
|
'--delete',
|
||||||
|
'-q',
|
||||||
|
'-W',
|
||||||
|
'-x',
|
||||||
|
f'{base_chroot_path}/',
|
||||||
|
f'{chroot_path}/',
|
||||||
|
])
|
||||||
|
if result.returncode != 0:
|
||||||
|
logging.fatal('Failed to sync chroot copy')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
os.makedirs(chroot_path + '/etc', exist_ok=True)
|
os.makedirs(chroot_path + '/etc', exist_ok=True)
|
||||||
shutil.copyfile(pacman_conf, pacman_conf_target)
|
shutil.copyfile(pacman_conf, pacman_conf_target)
|
||||||
|
|
||||||
|
@ -24,13 +51,21 @@ def create_chroot(chroot_name, packages=['base'], pacman_conf='/app/local/etc/pa
|
||||||
with open(pacman_conf_target, 'a') as file:
|
with open(pacman_conf_target, 'a') as file:
|
||||||
file.write(extra_conf)
|
file.write(extra_conf)
|
||||||
|
|
||||||
result = subprocess.run(['pacstrap', '-C', pacman_conf_target, '-c', '-G', chroot_path] + packages + [
|
result = subprocess.run([
|
||||||
|
'pacstrap',
|
||||||
|
'-C',
|
||||||
|
pacman_conf_target,
|
||||||
|
'-c',
|
||||||
|
'-G',
|
||||||
|
chroot_path,
|
||||||
|
] + packages + [
|
||||||
'--needed',
|
'--needed',
|
||||||
'--overwrite=*',
|
'--overwrite=*',
|
||||||
'-yyuu',
|
'-yyuu',
|
||||||
])
|
],
|
||||||
|
capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise Exception('Failed to install chroot')
|
raise Exception('Failed to install chroot:' + result.stdout + '\n' + result.stderr)
|
||||||
return chroot_path
|
return chroot_path
|
||||||
|
|
||||||
|
|
||||||
|
|
11
config.py
11
config.py
|
@ -25,11 +25,13 @@ CONFIG_DEFAULTS = {
|
||||||
'paths': {
|
'paths': {
|
||||||
'chroots': os.path.join(appdirs.user_cache_dir('kupfer'), 'chroots'),
|
'chroots': os.path.join(appdirs.user_cache_dir('kupfer'), 'chroots'),
|
||||||
'pacman': os.path.join(appdirs.user_cache_dir('kupfer'), 'pacman'),
|
'pacman': os.path.join(appdirs.user_cache_dir('kupfer'), 'pacman'),
|
||||||
'jumpdrive': os.path.join(appdirs.user_cache_dir('kupfer'), 'jumpdrive')
|
'jumpdrive': os.path.join(appdirs.user_cache_dir('kupfer'), 'jumpdrive'),
|
||||||
|
'packages': os.path.join(appdirs.user_cache_dir('kupfer'), 'packages'),
|
||||||
|
'pkgbuilds': os.path.abspath(os.getcwd()),
|
||||||
},
|
},
|
||||||
'profiles': {
|
'profiles': {
|
||||||
'default': deepcopy(PROFILE_DEFAULTS)
|
'default': deepcopy(PROFILE_DEFAULTS),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,6 +148,7 @@ class ConfigStateHolder:
|
||||||
|
|
||||||
file_state = ConfigLoadState()
|
file_state = ConfigLoadState()
|
||||||
|
|
||||||
|
defaults = CONFIG_DEFAULTS
|
||||||
# config options that are persisted to file
|
# config options that are persisted to file
|
||||||
file: dict = {}
|
file: dict = {}
|
||||||
# runtime config not persisted anywhere
|
# runtime config not persisted anywhere
|
||||||
|
@ -168,7 +171,7 @@ class ConfigStateHolder:
|
||||||
self.file_state.load_finished = True
|
self.file_state.load_finished = True
|
||||||
|
|
||||||
def is_loaded(self):
|
def is_loaded(self):
|
||||||
return self.file_state.load_finished and self.file_state.exception == None
|
return self.file_state.load_finished and self.file_state.exception is None
|
||||||
|
|
||||||
def enforce_config_loaded(self):
|
def enforce_config_loaded(self):
|
||||||
if not self.file_state.load_finished:
|
if not self.file_state.load_finished:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
PACMAN_CHROOT="${PACMAN_CHROOT:-/chroot/copy}"
|
PACMAN_CHROOT="${PACMAN_CHROOT:-/chroot/base_aarch64}"
|
||||||
|
|
||||||
exec pacman --root "$PACMAN_CHROOT" --arch aarch64 --config "$PACMAN_CHROOT"/etc/pacman.conf "$@"
|
exec pacman --root "$PACMAN_CHROOT" --arch aarch64 --config "$PACMAN_CHROOT"/etc/pacman.conf "$@"
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
#
|
#
|
||||||
#-- The download utilities that makepkg should use to acquire sources
|
#-- The download utilities that makepkg should use to acquire sources
|
||||||
# Format: 'protocol::agent'
|
# Format: 'protocol::agent'
|
||||||
DLAGENTS=('file::/usr/bin/curl -gqC - -o %o %u'
|
DLAGENTS=('file::/usr/bin/curl -qgC - -o %o %u'
|
||||||
'ftp::/usr/bin/curl -gqfC - --ftp-pasv --retry 3 --retry-delay 3 -o %o %u'
|
'ftp::/usr/bin/curl -qgfC - --ftp-pasv --retry 3 --retry-delay 3 -o %o %u'
|
||||||
'http::/usr/bin/curl -gqb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
|
'http::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
|
||||||
'https::/usr/bin/curl -gqb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
|
'https::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
|
||||||
'rsync::/usr/bin/rsync --no-motd -z %u %o'
|
'rsync::/usr/bin/rsync --no-motd -z %u %o'
|
||||||
'scp::/usr/bin/scp -C %u %o')
|
'scp::/usr/bin/scp -C %u %o')
|
||||||
|
|
||||||
|
@ -162,8 +162,9 @@ SRCEXT='.src.tar.gz'
|
||||||
export CROOT="/usr/aarch64-linux-gnu"
|
export CROOT="/usr/aarch64-linux-gnu"
|
||||||
export ARCH="arm64"
|
export ARCH="arm64"
|
||||||
export CROSS_COMPILE="aarch64-linux-gnu-"
|
export CROSS_COMPILE="aarch64-linux-gnu-"
|
||||||
export CC="aarch64-linux-gnu-gcc -I${CROOT}/usr/include -I/chroot/copy/usr/include -L${CROOT}/lib -L/chroot/copy/usr/lib"
|
export CC="aarch64-linux-gnu-gcc -I${CROOT}/usr/include -I/chroot/base_aarch64/usr/include -L${CROOT}/lib -L/chroot/base_aarch64/usr/lib"
|
||||||
export CXX="aarch64-linux-gnu-g++ -I${CROOT}/usr/include -I/chroot/copy/usr/include -L${CROOT}/lib -L/chroot/copy/usr/lib"
|
export CXX="aarch64-linux-gnu-g++ -I${CROOT}/usr/include -I/chroot/base_aarch64/usr/include -L${CROOT}/lib -L/chroot/base_aarch64/usr/lib"
|
||||||
export CFLAGS="$CFLAGS -I${CROOT}/usr/include -I/chroot/copy/usr/include"
|
export CFLAGS="$CFLAGS -I${CROOT}/usr/include -I/chroot/base_aarch64/usr/include"
|
||||||
export CXXFLAGS="$CXXFLAGS -I${CROOT}/usr/include -I/chroot/copy/usr/include"
|
export CXXFLAGS="$CXXFLAGS -I${CROOT}/usr/include -I/chroot/base_aarch64/usr/include"
|
||||||
export LDFLAGS="$LDFLAGS,-L${CROOT}/lib,-L/chroot/copy/usr/lib,-rpath-link,${CROOT}/lib,-rpath-link,/chroot/copy/usr/lib"
|
export LDFLAGS="$LDFLAGS,-L${CROOT}/lib,-L/chroot/base_aarch64/usr/lib,-rpath-link,${CROOT}/lib,-rpath-link,/chroot/base_aarch64/usr/lib"
|
||||||
|
export PACMAN_CHROOT="/chroot/base_aarch64"
|
||||||
|
|
|
@ -7,7 +7,7 @@ sed -i "s/@CHOST@/aarch64-unknown-linux-gnu/g" etc/makepkg.conf
|
||||||
sed -i "s/@CARCHFLAGS@/-march=armv8-a /g" etc/makepkg.conf
|
sed -i "s/@CARCHFLAGS@/-march=armv8-a /g" etc/makepkg.conf
|
||||||
sed -i "s/xz /xz -T0 /g" etc/makepkg.conf
|
sed -i "s/xz /xz -T0 /g" etc/makepkg.conf
|
||||||
sed -i "s/ check / !check /g" etc/makepkg.conf
|
sed -i "s/ check / !check /g" etc/makepkg.conf
|
||||||
chroot="/chroot/copy"
|
chroot="/chroot/base_aarch64"
|
||||||
include="-I\${CROOT}/usr/include -I$chroot/usr/include"
|
include="-I\${CROOT}/usr/include -I$chroot/usr/include"
|
||||||
lib_croot="\${CROOT}/lib"
|
lib_croot="\${CROOT}/lib"
|
||||||
lib_chroot="$chroot/usr/lib"
|
lib_chroot="$chroot/usr/lib"
|
||||||
|
@ -21,6 +21,7 @@ export CXX="aarch64-linux-gnu-g++ $include -L$lib_croot -L$lib_chroot"
|
||||||
export CFLAGS="\$CFLAGS $include"
|
export CFLAGS="\$CFLAGS $include"
|
||||||
export CXXFLAGS="\$CXXFLAGS $include"
|
export CXXFLAGS="\$CXXFLAGS $include"
|
||||||
export LDFLAGS="\$LDFLAGS,-L$lib_croot,-L$lib_chroot,-rpath-link,$lib_croot,-rpath-link,$lib_chroot"
|
export LDFLAGS="\$LDFLAGS,-L$lib_croot,-L$lib_chroot,-rpath-link,$lib_croot,-rpath-link,$lib_chroot"
|
||||||
|
export PACMAN_CHROOT="$chroot"
|
||||||
EOF
|
EOF
|
||||||
# TODO: Set PACKAGER
|
# TODO: Set PACKAGER
|
||||||
wget https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman/pacman.conf -O etc/pacman.conf
|
wget https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman/pacman.conf -O etc/pacman.conf
|
||||||
|
|
373
packages.py
373
packages.py
|
@ -37,20 +37,21 @@ pacman_cmd = [
|
||||||
|
|
||||||
class Package:
|
class Package:
|
||||||
name = ''
|
name = ''
|
||||||
names = []
|
names: list[str] = []
|
||||||
depends = []
|
depends: list[str] = []
|
||||||
local_depends = None
|
local_depends = None
|
||||||
repo = ''
|
repo = ''
|
||||||
mode = ''
|
mode = ''
|
||||||
|
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str, dir: str = None) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self._loadinfo()
|
dir = dir if dir else config['paths']['pkgbuilds']
|
||||||
|
self._loadinfo(dir)
|
||||||
|
|
||||||
def _loadinfo(self):
|
def _loadinfo(self, dir):
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
makepkg_cmd + ['--printsrcinfo'],
|
makepkg_cmd + ['--printsrcinfo'],
|
||||||
cwd=self.path,
|
cwd=os.path.join(dir, self.path),
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
lines = result.stdout.decode('utf-8').split('\n')
|
lines = result.stdout.decode('utf-8').split('\n')
|
||||||
|
@ -92,15 +93,17 @@ class Package:
|
||||||
return f'package({self.name},{repr(self.names)})'
|
return f'package({self.name},{repr(self.names)})'
|
||||||
|
|
||||||
|
|
||||||
def check_prebuilts():
|
local_packages: dict[Package] = None
|
||||||
if not os.path.exists('prebuilts'):
|
|
||||||
os.makedirs('prebuilts')
|
|
||||||
|
def check_prebuilts(dir: str = None):
|
||||||
|
prebuilts_dir = dir if dir else config.file['paths']['packages']
|
||||||
|
os.makedirs(prebuilts_dir, exist_ok=True)
|
||||||
for repo in REPOSITORIES:
|
for repo in REPOSITORIES:
|
||||||
if not os.path.exists(os.path.join('prebuilts', repo)):
|
os.makedirs(os.path.join(prebuilts_dir, repo), exist_ok=True)
|
||||||
os.makedirs(os.path.join('prebuilts', repo))
|
|
||||||
for ext1 in ['db', 'files']:
|
for ext1 in ['db', 'files']:
|
||||||
for ext2 in ['', '.tar.xz']:
|
for ext2 in ['', '.tar.xz']:
|
||||||
if not os.path.exists(os.path.join('prebuilts', repo, f'{repo}.{ext1}{ext2}')):
|
if not os.path.exists(os.path.join(prebuilts_dir, repo, f'{repo}.{ext1}{ext2}')):
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
'tar',
|
'tar',
|
||||||
|
@ -109,79 +112,23 @@ def check_prebuilts():
|
||||||
'-T',
|
'-T',
|
||||||
'/dev/null',
|
'/dev/null',
|
||||||
],
|
],
|
||||||
cwd=os.path.join('prebuilts', repo),
|
cwd=os.path.join(prebuilts_dir, repo),
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.fatal('Failed to create prebuilt repos')
|
logging.fatal('Failed to create prebuilt repos')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def setup_build_chroot(arch='aarch64'):
|
def discover_packages(package_paths: list[str] = ['all'], dir: str = None) -> dict[str, Package]:
|
||||||
chroot_name = f'build_{arch}'
|
dir = dir if dir else config.file['paths']['pkgbuilds']
|
||||||
logging.info('Initializing {arch} build chroot')
|
|
||||||
extra_repos = {}
|
|
||||||
for repo in REPOSITORIES:
|
|
||||||
extra_repos[repo] = {
|
|
||||||
'Server': f'file:///src/prebuilts/{repo}',
|
|
||||||
}
|
|
||||||
chroot_path = create_chroot(
|
|
||||||
chroot_name,
|
|
||||||
packages=['base-devel'],
|
|
||||||
pacman_conf='/app/local/etc/pacman.conf',
|
|
||||||
extra_repos=extra_repos,
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.info('Updating root chroot')
|
|
||||||
result = subprocess.run(pacman_cmd + [
|
|
||||||
'--root',
|
|
||||||
chroot_path,
|
|
||||||
'--arch',
|
|
||||||
arch,
|
|
||||||
'--config',
|
|
||||||
chroot_path + '/etc/pacman.conf',
|
|
||||||
])
|
|
||||||
if result.returncode != 0:
|
|
||||||
logging.fatal('Failed to update root chroot')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
with open('/chroot/root/usr/bin/makepkg', 'r') as file:
|
|
||||||
data = file.read()
|
|
||||||
data = data.replace('EUID == 0', 'EUID == -1')
|
|
||||||
with open('/chroot/root/usr/bin/makepkg', 'w') as file:
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
with open('/chroot/root/etc/makepkg.conf', 'r') as file:
|
|
||||||
data = file.read()
|
|
||||||
data = data.replace('xz -c', 'xz -T0 -c')
|
|
||||||
data = data.replace(' check ', ' !check ')
|
|
||||||
with open('/chroot/root/etc/makepkg.conf', 'w') as file:
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
logging.info('Syncing chroot copy')
|
|
||||||
result = subprocess.run([
|
|
||||||
'rsync',
|
|
||||||
'-a',
|
|
||||||
'--delete',
|
|
||||||
'-q',
|
|
||||||
'-W',
|
|
||||||
'-x',
|
|
||||||
'/chroot/root/',
|
|
||||||
'/chroot/copy',
|
|
||||||
])
|
|
||||||
if result.returncode != 0:
|
|
||||||
logging.fatal('Failed to sync chroot copy')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def discover_packages(package_paths: list[str]) -> dict[str, Package]:
|
|
||||||
packages = {}
|
packages = {}
|
||||||
paths = []
|
paths = []
|
||||||
|
|
||||||
for repo in REPOSITORIES:
|
for repo in REPOSITORIES:
|
||||||
for dir in os.listdir(repo):
|
for _dir in os.listdir(os.path.join(dir, repo)):
|
||||||
paths.append(os.path.join(repo, dir))
|
paths.append(os.path.join(repo, _dir))
|
||||||
|
|
||||||
results = Parallel(n_jobs=multiprocessing.cpu_count() * 4)(delayed(Package)(path) for path in paths)
|
results = Parallel(n_jobs=multiprocessing.cpu_count() * 4)(delayed(Package)(path, dir) for path in paths)
|
||||||
for package in results:
|
for package in results:
|
||||||
packages[package.name] = package
|
packages[package.name] = package
|
||||||
|
|
||||||
|
@ -189,57 +136,121 @@ def discover_packages(package_paths: list[str]) -> dict[str, Package]:
|
||||||
for package in packages.values():
|
for package in packages.values():
|
||||||
package.local_depends = package.depends.copy()
|
package.local_depends = package.depends.copy()
|
||||||
for dep in package.depends.copy():
|
for dep in package.depends.copy():
|
||||||
found = False
|
found = dep in packages
|
||||||
for p in packages.values():
|
for p in packages.values():
|
||||||
|
if found:
|
||||||
|
break
|
||||||
for name in p.names:
|
for name in p.names:
|
||||||
if dep == name:
|
if dep == name:
|
||||||
|
logging.debug(f'Found {p.name} that provides {dep}')
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if found:
|
|
||||||
break
|
|
||||||
if not found:
|
if not found:
|
||||||
logging.debug(f'Removing {dep} from dependencies')
|
logging.debug(f'Removing {dep} from dependencies')
|
||||||
package.local_depends.remove(dep)
|
package.local_depends.remove(dep)
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def filter_packages_by_paths(repo: list[Package], paths: list[str]) -> list[Package]:
|
||||||
|
if 'all' in paths:
|
||||||
|
return repo.values()
|
||||||
|
result = []
|
||||||
|
for pkg in repo:
|
||||||
|
if pkg.path in paths:
|
||||||
|
result += [pkg]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generate_dependency_chain(package_repo: dict[str, Package], to_build: list[Package]) -> list[set[Package]]:
|
||||||
"""
|
"""
|
||||||
This figures out all dependencies and their sub-dependencies for the selection and adds those packages to the selection.
|
This figures out all dependencies and their sub-dependencies for the selection and adds those packages to the selection.
|
||||||
First the top-level packages get selected by searching the paths.
|
First the top-level packages get selected by searching the paths.
|
||||||
Then their dependencies and sub-dependencies and so on get added to the selection.
|
Then their dependencies and sub-dependencies and so on get added to the selection.
|
||||||
"""
|
"""
|
||||||
selection = []
|
visited = set[Package]()
|
||||||
deps = []
|
visited_names = set[str]()
|
||||||
for package in packages.values():
|
dep_levels: list[set[Package]] = [set(), set()]
|
||||||
if 'all' in package_paths or package.path in package_paths:
|
|
||||||
deps.append(package.name)
|
|
||||||
while len(deps) > 0:
|
|
||||||
for dep in deps.copy():
|
|
||||||
found = False
|
|
||||||
for p in selection:
|
|
||||||
for name in p.names:
|
|
||||||
if name == dep:
|
|
||||||
deps.remove(dep)
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
for p in packages.values():
|
|
||||||
if found:
|
|
||||||
break
|
|
||||||
for name in p.names:
|
|
||||||
if name == dep:
|
|
||||||
selection.append(packages[p.name])
|
|
||||||
deps.remove(dep)
|
|
||||||
# Add the sub-dependencies
|
|
||||||
deps += p.local_depends
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
logging.fatal(f'Failed to find dependency {dep}')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
selection = list(set(selection))
|
def visited(package: Package, visited=visited, visited_names=visited_names):
|
||||||
packages = {package.name: package for package in selection}
|
visited.add(package)
|
||||||
|
visited_names.update(package.names)
|
||||||
|
|
||||||
logging.debug(f'Figured out selection: {list(map(lambda p: p.path, selection))}')
|
def join_levels(levels: list[set[Package]]) -> dict[Package, int]:
|
||||||
|
result = dict[Package, int]()
|
||||||
|
for i, level in enumerate(levels):
|
||||||
|
result[level] = i
|
||||||
|
|
||||||
return packages
|
# init level 0
|
||||||
|
for package in to_build:
|
||||||
|
visited(package)
|
||||||
|
dep_levels[0].add(package)
|
||||||
|
# add dependencies of our requested builds to level 0
|
||||||
|
for dep_name in package.depends:
|
||||||
|
if dep_name in visited_names:
|
||||||
|
continue
|
||||||
|
elif dep_name in package_repo:
|
||||||
|
dep_pkg = package_repo[dep_name]
|
||||||
|
logging.debug(f"Adding {package.name}'s dependency {dep_name} to level 0")
|
||||||
|
dep_levels[0].add(dep_pkg)
|
||||||
|
visited(dep_pkg)
|
||||||
|
logging.debug('Generating dependency chain:')
|
||||||
|
"""
|
||||||
|
Starting with `level` = 0, iterate over the packages in `dep_levels[level]`:
|
||||||
|
1. Moving packages that are dependencies of other packages up to `level`+1
|
||||||
|
2. Adding yet unadded local dependencies of all pkgs on `level` to `level`+1
|
||||||
|
3. increment level
|
||||||
|
4. repeat until
|
||||||
|
"""
|
||||||
|
level = 0
|
||||||
|
# protect against dependency cycles
|
||||||
|
repeat_count = 0
|
||||||
|
_last_level: set[Package] = None
|
||||||
|
while dep_levels[level]:
|
||||||
|
logging.debug(f'Scanning dependency level {level}')
|
||||||
|
if level > 100:
|
||||||
|
raise Exception('Dependency chain reached 100 levels depth, this is probably a bug. Aborting!')
|
||||||
|
|
||||||
|
for pkg in dep_levels[level].copy():
|
||||||
|
pkg_done = False
|
||||||
|
if pkg not in dep_levels[level]:
|
||||||
|
# pkg has been moved, move on
|
||||||
|
continue
|
||||||
|
# move pkg to level+1 if something else depends on it
|
||||||
|
for other_pkg in dep_levels[level].copy():
|
||||||
|
if pkg == other_pkg:
|
||||||
|
continue
|
||||||
|
if pkg_done:
|
||||||
|
break
|
||||||
|
if type(other_pkg) != Package:
|
||||||
|
logging.fatal('Wtf, this is not a package:' + repr(other_pkg))
|
||||||
|
for dep_name in other_pkg.depends:
|
||||||
|
if dep_name in pkg.names:
|
||||||
|
dep_levels[level].remove(pkg)
|
||||||
|
dep_levels[level + 1].add(pkg)
|
||||||
|
logging.debug(f'Moving {pkg.name} to level {level+1} because {other_pkg.name} depends on it as {dep_name}')
|
||||||
|
pkg_done = True
|
||||||
|
break
|
||||||
|
for dep_name in pkg.depends:
|
||||||
|
if dep_name in visited_names:
|
||||||
|
continue
|
||||||
|
elif dep_name in package_repo:
|
||||||
|
dep_pkg = package_repo[dep_name]
|
||||||
|
logging.debug(f"Adding {pkg.name}'s dependency {dep_name} to level {level+1}")
|
||||||
|
dep_levels[level + 1].add(dep_pkg)
|
||||||
|
visited(dep_pkg)
|
||||||
|
|
||||||
|
if _last_level == dep_levels[level]:
|
||||||
|
repeat_count += 1
|
||||||
|
else:
|
||||||
|
repeat_count = 0
|
||||||
|
if repeat_count > 10:
|
||||||
|
raise Exception(f'Probable dependency cycle detected: Level has been passed on unmodifed multiple times: #{level}: {_last_level}')
|
||||||
|
_last_level = dep_levels[level]
|
||||||
|
level += 1
|
||||||
|
dep_levels.append(set[Package]())
|
||||||
|
# reverse level list into buildorder (deps first!), prune empty levels
|
||||||
|
return list([lvl for lvl in dep_levels[::-1] if lvl])
|
||||||
|
|
||||||
|
|
||||||
def generate_package_order(packages: list[Package]) -> list[Package]:
|
def generate_package_order(packages: list[Package]) -> list[Package]:
|
||||||
|
@ -298,12 +309,61 @@ def check_package_version_built(package: Package) -> bool:
|
||||||
return built
|
return built
|
||||||
|
|
||||||
|
|
||||||
def setup_dependencies_and_sources(package: Package, enable_crosscompile: bool = True):
|
def setup_build_chroot(arch='aarch64') -> str:
|
||||||
logging.info(f'Setting up dependencies and sources for {package.path}')
|
chroot_name = f'build_{arch}'
|
||||||
|
logging.info(f'Initializing {arch} build chroot')
|
||||||
|
extra_repos = {}
|
||||||
|
for repo in REPOSITORIES:
|
||||||
|
extra_repos[repo] = {
|
||||||
|
'Server': f"file://{config.file['paths']['packages']}/{repo}",
|
||||||
|
}
|
||||||
|
chroot_path = create_chroot(
|
||||||
|
chroot_name,
|
||||||
|
packages=['base-devel'],
|
||||||
|
pacman_conf='/app/local/etc/pacman.conf',
|
||||||
|
extra_repos=extra_repos,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(f'Updating chroot {chroot_name}')
|
||||||
|
result = subprocess.run(
|
||||||
|
pacman_cmd + [
|
||||||
|
'--root',
|
||||||
|
chroot_path,
|
||||||
|
'--arch',
|
||||||
|
arch,
|
||||||
|
'--config',
|
||||||
|
chroot_path + '/etc/pacman.conf',
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logging.fatal(f'Failed to update chroot {chroot_name}:')
|
||||||
|
logging.fatal(result.stdout)
|
||||||
|
logging.fatal(result.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open(f'{chroot_path}/usr/bin/makepkg', 'r') as file:
|
||||||
|
data = file.read()
|
||||||
|
data = data.replace('EUID == 0', 'EUID == -1')
|
||||||
|
with open(f'{chroot_path}/usr/bin/makepkg', 'w') as file:
|
||||||
|
file.write(data)
|
||||||
|
|
||||||
|
with open(f'{chroot_path}/etc/makepkg.conf', 'r') as file:
|
||||||
|
data = file.read()
|
||||||
|
data = data.replace('xz -c', 'xz -T0 -c')
|
||||||
|
data = data.replace(' check ', ' !check ')
|
||||||
|
with open(f'{chroot_path}/etc/makepkg.conf', 'w') as file:
|
||||||
|
file.write(data)
|
||||||
|
return chroot_path
|
||||||
|
|
||||||
|
|
||||||
|
def setup_dependencies_and_sources(package: Package, chroot: str, repo_dir: str = None, enable_crosscompile: bool = True):
|
||||||
|
logging.info(f'Setting up dependencies and sources for {package.path} in {chroot}')
|
||||||
"""
|
"""
|
||||||
To make cross-compilation work for almost every package, the host needs to have the dependencies installed
|
To make cross-compilation work for almost every package, the host needs to have the dependencies installed
|
||||||
so that the build tools can be used
|
so that the build tools can be used
|
||||||
"""
|
"""
|
||||||
|
repo_dir = repo_dir if repo_dir else config.file['paths']['pkgbuilds']
|
||||||
if package.mode == 'cross' and enable_crosscompile:
|
if package.mode == 'cross' and enable_crosscompile:
|
||||||
for p in package.depends:
|
for p in package.depends:
|
||||||
# Don't check for errors here because there might be packages that are listed as dependencies but are not available on x86_64
|
# Don't check for errors here because there might be packages that are listed as dependencies but are not available on x86_64
|
||||||
|
@ -313,28 +373,28 @@ def setup_dependencies_and_sources(package: Package, enable_crosscompile: bool =
|
||||||
)
|
)
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
makepkg_cmd + [
|
[os.path.join(chroot, 'usr/bin/makepkg')] + makepkg_cmd[1:] + [
|
||||||
'--nobuild',
|
'--nobuild',
|
||||||
'--holdver',
|
'--holdver',
|
||||||
'--syncdeps',
|
'--syncdeps',
|
||||||
],
|
],
|
||||||
env=makepkg_cross_env,
|
env=makepkg_cross_env | {'PACMAN_CHROOT': chroot},
|
||||||
cwd=package.path,
|
cwd=os.path.join(repo_dir, package.path),
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.fatal(f'Failed to check sources for {package.path}')
|
raise Exception(f'Failed to check sources for {package.path}')
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def build_package(package: Package, enable_crosscompile: bool = True):
|
def build_package(package: Package, repo_dir: str = None, arch='aarch64', enable_crosscompile: bool = True):
|
||||||
makepkg_compile_opts = [
|
makepkg_compile_opts = [
|
||||||
'--noextract',
|
'--noextract',
|
||||||
'--skipinteg',
|
'--skipinteg',
|
||||||
'--holdver',
|
'--holdver',
|
||||||
'--nodeps',
|
'--nodeps',
|
||||||
]
|
]
|
||||||
|
repo_dir = repo_dir if repo_dir else config.file['paths']['pkgbuilds']
|
||||||
setup_dependencies_and_sources(package, enable_crosscompile=enable_crosscompile)
|
chroot = setup_build_chroot(arch=arch)
|
||||||
|
setup_dependencies_and_sources(package, chroot, enable_crosscompile=enable_crosscompile)
|
||||||
|
|
||||||
if package.mode == 'cross' and enable_crosscompile:
|
if package.mode == 'cross' and enable_crosscompile:
|
||||||
logging.info(f'Cross-compiling {package.path}')
|
logging.info(f'Cross-compiling {package.path}')
|
||||||
|
@ -349,67 +409,44 @@ def build_package(package: Package, enable_crosscompile: bool = True):
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
base_chroot = os.path.join(config.file['paths']['chroots'], f'base_{arch}')
|
||||||
result = subprocess.run([
|
result = subprocess.run([
|
||||||
'mount',
|
'mount',
|
||||||
'-o',
|
'-o',
|
||||||
'bind',
|
'bind',
|
||||||
'/chroot/copy/usr/share/i18n/locales',
|
f"{base_chroot}/usr/share/i18n/locales",
|
||||||
'/usr/share/i18n/locales',
|
'/usr/share/i18n/locales',
|
||||||
])
|
])
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.fatal(f'Failed to bind mount glibc locales from chroot')
|
logging.fatal(f'Failed to bind mount glibc locales from chroot {base_chroot}')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
makepkg_cmd + makepkg_compile_opts,
|
[os.path.join(chroot, 'usr/bin/makepkg')] + makepkg_cmd[1:] + makepkg_compile_opts,
|
||||||
env=makepkg_cross_env | {'QEMU_LD_PREFIX': '/usr/aarch64-linux-gnu'},
|
env=makepkg_cross_env | {'QEMU_LD_PREFIX': '/usr/aarch64-linux-gnu'},
|
||||||
cwd=package.path,
|
cwd=os.path.join(dir, package.path),
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.fatal(f'Failed to cross-compile package {package.path}')
|
logging.fatal(f'Failed to cross-compile package {package.path}')
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
logging.info(f'Host-compiling {package.path}')
|
logging.info(f'Host-compiling {package.path}')
|
||||||
|
os.makedirs(f'{chroot}/src')
|
||||||
def umount():
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
'umount',
|
|
||||||
'-lc',
|
|
||||||
'/chroot/copy',
|
|
||||||
],
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
atexit.register(umount)
|
|
||||||
|
|
||||||
result = subprocess.run([
|
result = subprocess.run([
|
||||||
'mount',
|
'mount',
|
||||||
'-o',
|
'-o',
|
||||||
'bind',
|
'bind',
|
||||||
'/chroot/copy',
|
config.file['paths']['pkgbuilds'],
|
||||||
'/chroot/copy',
|
f'{chroot}/src',
|
||||||
])
|
])
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.fatal('Failed to bind mount chroot to itself')
|
logging.fatal(f'Failed to bind mount pkgdirs to {chroot}/src')
|
||||||
exit(1)
|
|
||||||
|
|
||||||
os.makedirs('/chroot/copy/src')
|
|
||||||
result = subprocess.run([
|
|
||||||
'mount',
|
|
||||||
'-o',
|
|
||||||
'bind',
|
|
||||||
'.',
|
|
||||||
'/chroot/copy/src',
|
|
||||||
])
|
|
||||||
if result.returncode != 0:
|
|
||||||
logging.fatal(f'Failed to bind mount folder to chroot')
|
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
env = [f'{key}={value}' for key, value in makepkg_env.items()]
|
env = [f'{key}={value}' for key, value in makepkg_env.items()]
|
||||||
result = subprocess.run([
|
result = subprocess.run([
|
||||||
'arch-chroot',
|
'arch-chroot',
|
||||||
'/chroot/copy',
|
chroot,
|
||||||
'/usr/bin/env',
|
'/usr/bin/env',
|
||||||
] + env + [
|
] + env + [
|
||||||
'/bin/bash',
|
'/bin/bash',
|
||||||
|
@ -471,22 +508,34 @@ def cmd_build(paths, arch='aarch64'):
|
||||||
check_prebuilts()
|
check_prebuilts()
|
||||||
|
|
||||||
paths = list(paths)
|
paths = list(paths)
|
||||||
packages = discover_packages(paths)
|
repo = discover_packages()
|
||||||
|
|
||||||
package_order = generate_package_order(list(packages.values()))
|
package_levels = generate_dependency_chain(
|
||||||
need_build = []
|
repo,
|
||||||
for package in package_order:
|
filter_packages_by_paths(repo, paths),
|
||||||
if not check_package_version_built(package):
|
)
|
||||||
need_build.append(package)
|
build_names = set[str]()
|
||||||
|
build_levels = list[set[Package]]()
|
||||||
|
i = 0
|
||||||
|
for packages in package_levels:
|
||||||
|
level = set[Package]()
|
||||||
|
for package in packages:
|
||||||
|
if not check_package_version_built(package) or package.depends in build_names:
|
||||||
|
level.add(package)
|
||||||
|
build_names.update(package.names)
|
||||||
|
if level:
|
||||||
|
build_levels.append(level)
|
||||||
|
logging.debug(f'Adding to level {i}:' + '\n' + ('\n'.join([p.path for p in level])))
|
||||||
|
i += 1
|
||||||
|
|
||||||
if len(need_build) == 0:
|
if not build_levels:
|
||||||
logging.info('Everything built already')
|
logging.info('Everything built already')
|
||||||
return
|
return
|
||||||
logging.info('Building %s', ', '.join(map(lambda x: x.path, need_build)))
|
for level, need_build in enumerate(build_levels):
|
||||||
|
logging.info(f"(Level {level}) Building {', '.join([x.path for x in need_build])}")
|
||||||
crosscompile = config.file['build']['crosscompile']
|
crosscompile = config.file['build']['crosscompile']
|
||||||
for package in need_build:
|
for package in need_build:
|
||||||
setup_build_chroot(arch=arch)
|
build_package(package, arch=arch, enable_crosscompile=crosscompile)
|
||||||
build_package(package, enable_crosscompile=crosscompile)
|
|
||||||
add_package_to_repo(package)
|
add_package_to_repo(package)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ DOCKER_PATHS = {
|
||||||
'chroots': '/chroot',
|
'chroots': '/chroot',
|
||||||
'jumpdrive': '/var/cache/jumpdrive',
|
'jumpdrive': '/var/cache/jumpdrive',
|
||||||
'pacman': '/var/cache/pacman/pkg',
|
'pacman': '/var/cache/pacman/pkg',
|
||||||
|
'packages': '/prebuilts',
|
||||||
|
'pkgbuilds': '/src',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,8 +23,9 @@ def wrap_docker():
|
||||||
for source, destination in volume_mappings.items():
|
for source, destination in volume_mappings.items():
|
||||||
result += ['-v', f'{source}:{destination}:z']
|
result += ['-v', f'{source}:{destination}:z']
|
||||||
return result
|
return result
|
||||||
|
os.readl
|
||||||
|
|
||||||
script_path = os.path.dirname(os.path.abspath(__file__))
|
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
with open(os.path.join(script_path, 'version.txt')) as version_file:
|
with open(os.path.join(script_path, 'version.txt')) as version_file:
|
||||||
version = version_file.read().replace('\n', '')
|
version = version_file.read().replace('\n', '')
|
||||||
tag = f'registry.gitlab.com/kupfer/kupferbootstrap:{version}'
|
tag = f'registry.gitlab.com/kupfer/kupferbootstrap:{version}'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue