mirror of
https://github.com/archlinux-jerry/buildbot
synced 2024-11-22 13:00:40 +08:00
buildbot: repo.py
This commit is contained in:
commit
1509183847
8 changed files with 317 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
.vscode/*
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
buildbot.log*
|
||||
buildbot.sql
|
||||
test
|
25
README.md
Normal file
25
README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Buildbot
|
||||
|
||||
## Typical autobuild.yaml format
|
||||
### Note
|
||||
Anything starting with bash will be considered as bash commands.
|
||||
`e.g. - bash ls -al`
|
||||
All of the four blocks: updates, prebuild, build, postbuild can be ommited, and their first value will be used.
|
||||
### Example
|
||||
```
|
||||
updates:
|
||||
- repo (repo only, it means the package will only be built when a new commit is pushed to repo.)
|
||||
- git <url> <remote/branch>* (* means optional)
|
||||
- ?? (tbd)
|
||||
prebuild:
|
||||
- standard (do nothing)
|
||||
- git-cherrypick <url> <remote/branch> <local_branch> push*
|
||||
- ??
|
||||
build:
|
||||
- standard (makepkg -s, note that missing aur dependencies will not be resolved.)
|
||||
- ??
|
||||
postbuild:
|
||||
- standard (sign and upload)
|
||||
- do_nothing (leave it alone)
|
||||
- ??
|
||||
```
|
8
config.py
Normal file
8
config.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
# config
|
||||
REPO_NAME='jerryxiao'
|
||||
PKG_COMPRESSION='xz'
|
||||
|
||||
ARCHS = ['aarch64', 'any', 'armv7h', 'x86_64']
|
||||
BUILD_ARCHS = ['aarch64', 'any', 'x86_64']
|
||||
|
||||
REPO_CMD = 'repo-add --verify --remove'
|
2
master.py
Executable file
2
master.py
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
13
package.py
Normal file
13
package.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from utils import bash
|
||||
|
||||
logger = logging.getLogger(name='package')
|
||||
|
||||
|
||||
# makepkg -o
|
||||
# makepkg -e
|
||||
# makepkg --nosign
|
||||
# makepkg --packagelist
|
||||
# gpg --default-key {GPG_KEY} --no-armor --pinentry-mode loopback --passphrase '' --detach-sign --yes -- aaa
|
131
repo.py
Executable file
131
repo.py
Executable file
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# repo.py: Automatic management tool for an arch repo.
|
||||
# This file is part of Buildbot by JerryXiao
|
||||
|
||||
# Directory structure of the repo:
|
||||
# buildbot -- buildbot (git)
|
||||
# buildbot/repo -- repo root
|
||||
# /updates/ -- new packages goes in here
|
||||
# /updates/archive -- archive dir, old packages goes in here
|
||||
# /www/ -- http server root
|
||||
# /www/archive => /updates/archive -- archive dir for users
|
||||
# /www/aarch64 -- packages for "aarch64"
|
||||
# /www/any -- packages for "any"
|
||||
# /www/armv7h -- packages for "armv7h" (No build bot)
|
||||
# /www/x86_64 -- packages for "x86_64"
|
||||
# /www/robots.txt => /r_r_n/r.txt -- robots.txt
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from utils import bash, Pkg, get_pkg_details_from_name
|
||||
from time import time
|
||||
|
||||
from config import REPO_NAME, PKG_COMPRESSION, ARCHS, REPO_CMD
|
||||
|
||||
abspath = os.path.abspath(__file__)
|
||||
repocwd = Path(abspath).parent / 'repo'
|
||||
repocwd.mkdir(mode=0o755, exist_ok=True)
|
||||
os.chdir(repocwd)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def symlink(dst, src, exist_ok=True):
|
||||
assert issubclass(type(dst), os.PathLike) and type(src) is str
|
||||
try:
|
||||
dst.symlink_to(src)
|
||||
except FileExistsError:
|
||||
if not exist_ok:
|
||||
raise
|
||||
|
||||
def checkenv():
|
||||
(Path(abspath).parent / 'recycled').mkdir(mode=0o755, exist_ok=True)
|
||||
dirs = [Path('updates/archive')] + [Path('www/') / arch for arch in ARCHS]
|
||||
for mydir in dirs:
|
||||
mydir.mkdir(mode=0o755, exist_ok=True, parents=True)
|
||||
try:
|
||||
symlink(Path('www/archive'), 'updates/archive')
|
||||
except FileExistsError:
|
||||
pass
|
||||
checkenv()
|
||||
|
||||
|
||||
def repo_add(fpath):
|
||||
assert issubclass(type(fpath), os.PathLike) and fpath.name.endswith(f'.pkg.tar.{PKG_COMPRESSION}')
|
||||
dbpath = fpath.parent / f'{REPO_NAME}.db.tar.gz'
|
||||
return bash(f'{REPO_CMD} {dbpath} {fpath}')
|
||||
|
||||
def throw_away(fpath):
|
||||
assert issubclass(type(fpath), os.PathLike)
|
||||
newPath = Path(abspath).parent / 'recycled' / f"{fpath.name}_{time()}"
|
||||
assert not newPath.exists()
|
||||
fpath.rename(newPath)
|
||||
|
||||
def _check_repo():
|
||||
rn = REPO_NAME
|
||||
repo_files = (f"{rn}.db {rn}.db.tar.gz {rn}.db.tar.gz.old "
|
||||
f"{rn}.files {rn}.files.tar.gz {rn}.files.tar.gz.old")
|
||||
repo_files = repo_files.split(' ')
|
||||
repo_files_essential = [fname for fname in repo_files if not fname.endswith('.old')]
|
||||
assert repo_files_essential
|
||||
# make symlink for arch=any pkgs
|
||||
basedir = Path('www') / 'any'
|
||||
if basedir.exists():
|
||||
for pkgfile in basedir.iterdir():
|
||||
if pkgfile.name.endswith(f'.pkg.tar.{PKG_COMPRESSION}') and \
|
||||
get_pkg_details_from_name(pkgfile.name).arch == 'any':
|
||||
sigfile = Path(f"{str(pkgfile)}.sig")
|
||||
if sigfile.exists():
|
||||
logger.info(f'Creating symlink for {pkgfile}, {sigfile}')
|
||||
for arch in ARCHS:
|
||||
if arch == 'any':
|
||||
continue
|
||||
symlink(pkgfile.parent / '..' / arch / pkgfile.name, f'../any/{pkgfile.name}')
|
||||
symlink(sigfile.parent / '..' / arch / sigfile.name, f'../any/{sigfile.name}')
|
||||
else:
|
||||
logger.error(f'{arch} dir does not exist!')
|
||||
# run repo_add
|
||||
for arch in ARCHS:
|
||||
basedir = Path('www') / arch
|
||||
repo_files_count = list()
|
||||
if not basedir.exists():
|
||||
logger.error(f'{arch} dir does not exist!')
|
||||
continue
|
||||
pkgfiles = [f for f in basedir.iterdir()]
|
||||
for pkgfile in pkgfiles:
|
||||
if pkgfile.name in repo_files:
|
||||
repo_files_count.append(pkgfile.name)
|
||||
continue
|
||||
if pkgfile.name.endswith(f'.pkg.tar.{PKG_COMPRESSION}.sig'):
|
||||
if not Path(str(pkgfile)[:-4]).exists() and pkgfile.exists():
|
||||
logger.warning(f"{pkgfile} has no package!")
|
||||
throw_away(pkgfile)
|
||||
continue
|
||||
elif pkgfile.name.endswith(f'.pkg.tar.{PKG_COMPRESSION}'):
|
||||
sigfile = Path(f"{str(pkgfile)}.sig")
|
||||
if not sigfile.exists():
|
||||
logger.warning(f"{pkgfile} has no signature!")
|
||||
throw_away(pkgfile)
|
||||
continue
|
||||
realarch = get_pkg_details_from_name(pkgfile.name).arch
|
||||
if realarch != 'any' and realarch != arch:
|
||||
newpath = pkgfile.parent / '..' / realarch / pkgfile.name
|
||||
assert not newpath.exists()
|
||||
pkgfile.rename(newpath)
|
||||
newSigpath = pkgfile.parent / '..' / realarch / f"{pkgfile.name}.sig"
|
||||
assert not newSigpath.exists()
|
||||
sigfile.rename(newSigpath)
|
||||
logger.info(f'Moving {pkgfile} to {newpath}, {sigfile} to {newSigpath}')
|
||||
logger.debug("repo-add: %s", repo_add(newpath))
|
||||
else:
|
||||
logger.debug("repo-add: %s", repo_add(pkgfile))
|
||||
else:
|
||||
logger.warning(f"{pkgfile} is garbage!")
|
||||
throw_away(pkgfile)
|
||||
for rfile in repo_files_essential:
|
||||
if rfile not in repo_files_count:
|
||||
logger.error(f'{rfile} does not exist in {arch}!')
|
||||
|
108
utils.py
Normal file
108
utils.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import logging
|
||||
from time import time
|
||||
import re
|
||||
from threading import Thread, Lock
|
||||
|
||||
from config import PKG_COMPRESSION
|
||||
|
||||
logger = logging.getLogger(name='utils')
|
||||
|
||||
def background(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
tr = Thread(target=func, args=args, kwargs=kwargs)
|
||||
tr.daemon = True
|
||||
tr.start()
|
||||
return tr
|
||||
return wrapped
|
||||
|
||||
def bash(cmdline, **kwargs):
|
||||
assert type(cmdline) is str
|
||||
logger.info(f'bash: {cmdline}')
|
||||
return(run_cmd(['/bin/bash', '-x', '-e', '-c', cmdline], **kwargs))
|
||||
|
||||
def long_bash(cmdline, hours=2):
|
||||
assert type(hours) is int and hours >= 1
|
||||
logger.info(f'longbash{hours}: {cmdline}')
|
||||
return bash(cmdline, keepalive=True, KEEPALIVE_TIMEOUT=60, RUN_CMD_TIMEOUT=hours*60*60)
|
||||
|
||||
def run_cmd(cmd, keepalive=False, KEEPALIVE_TIMEOUT=30, RUN_CMD_TIMEOUT=60):
|
||||
RUN_CMD_LOOP_TIME = KEEPALIVE_TIMEOUT - 1 if KEEPALIVE_TIMEOUT >= 10 else 5
|
||||
stopped = False
|
||||
last_read = [int(time()), ""]
|
||||
output = list()
|
||||
stdout_lock = Lock()
|
||||
@background
|
||||
def check_stdout(stdout):
|
||||
nonlocal stopped, last_read, output
|
||||
stdout_lock.acquire()
|
||||
last_read_time = int(time())
|
||||
while stopped is False:
|
||||
line = stdout.readline(4096)
|
||||
last_read_time = int(time())
|
||||
logger.debug(line)
|
||||
output.append(line)
|
||||
last_read[0] = last_read_time
|
||||
last_read[1] = line
|
||||
stdout_lock.release()
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, encoding='utf-8')
|
||||
check_stdout(p.stdout)
|
||||
process_start = int(time())
|
||||
while True:
|
||||
try:
|
||||
p.wait(timeout=RUN_CMD_LOOP_TIME)
|
||||
except subprocess.TimeoutExpired:
|
||||
time_passed = int(time()) - last_read[0]
|
||||
if time_passed >= KEEPALIVE_TIMEOUT*2:
|
||||
logger.info('Timeout expired. No action.')
|
||||
output.append('+ Buildbot: Timeout expired. No action.\n')
|
||||
elif time_passed >= KEEPALIVE_TIMEOUT:
|
||||
if keepalive:
|
||||
logger.info('Timeout expired, writing nl')
|
||||
output.append('+ Buildbot: Timeout expired, writing nl\n')
|
||||
p.stdin.write('\n')
|
||||
p.stdin.flush()
|
||||
else:
|
||||
logger.info('Timeout expired, not writing nl')
|
||||
output.append('+ Buildbot: Timeout expired, not writing nl\n')
|
||||
if int(time()) - process_start >= RUN_CMD_TIMEOUT:
|
||||
stopped = True
|
||||
logger.error('Process timeout expired, terminating.')
|
||||
output.append('+ Buildbot: Process timeout expired, terminating.\n')
|
||||
p.terminate()
|
||||
try:
|
||||
p.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error('Cannot terminate, killing.')
|
||||
output.append('+ Buildbot: Cannot terminate, killing.\n')
|
||||
p.kill()
|
||||
break
|
||||
else:
|
||||
stopped = True
|
||||
break
|
||||
code = p.returncode
|
||||
|
||||
stdout_lock.acquire(10)
|
||||
outstr = ''.join(output)
|
||||
|
||||
if code != 0:
|
||||
raise subprocess.CalledProcessError(code, cmd, outstr)
|
||||
return outstr
|
||||
|
||||
class Pkg:
|
||||
def __init__(self, pkgname, pkgver, pkgrel, arch):
|
||||
self.pkgname = pkgname
|
||||
self.pkgver = pkgver
|
||||
self.pkgrel = pkgrel
|
||||
self.arch = arch
|
||||
|
||||
def get_pkg_details_from_name(name):
|
||||
if name.endswith(f'pkg.tar.{PKG_COMPRESSION}'):
|
||||
arch = re.match(r'(.+)-([^-]+)-([^-]+)-([^-]+)\.pkg\.tar\.\w+', name)
|
||||
assert arch and arch.groups() and len(arch.groups()) == 4
|
||||
(pkgname, pkgver, pkgrel, arch) = arch.groups()
|
||||
return Pkg(pkgname, pkgver, pkgrel, arch)
|
||||
|
24
worker.py
Executable file
24
worker.py
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from utils import bash
|
||||
from yaml import load, dump
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
abspath=os.path.abspath(__file__)
|
||||
abspath=os.path.dirname(abspath)
|
||||
os.chdir(abspath)
|
||||
|
||||
# include all autobuild.yaml files
|
||||
|
||||
REPO_NAME = Path('repo')
|
||||
BUTOBUILD_FNAME = 'autobuild.yaml'
|
||||
for mydir in REPO_NAME.iterdir():
|
||||
if mydir.is_dir() and (mydir / BUTOBUILD_FNAME).exists():
|
||||
# parsing yaml
|
||||
logger.info('Bulidbot: found %s in %s', BUTOBUILD_FNAME, mydir / BUTOBUILD_FNAME)
|
||||
|
Loading…
Reference in a new issue