eobuilder/eobuilder-ctl

566 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
import atexit
import random
import re
import shutil
import subprocess
import sys
import tarfile
import time
import urllib.parse
import os
from eobuilder import settings, VERSION, init
from eobuilder.changelog import changelog_from_git
from eobuilder.cmdline import parse_cmdline, error, cat, touch, call, output, setup_py
# fix urljoin for git+ssh:// URLs
if 'git+ssh' not in urllib.parse.uses_relative:
urllib.parse.uses_relative.append('git+ssh')
def rm_recursive(path):
if os.path.exists(path):
shutil.rmtree(path)
def smart_cleaning(files_path):
now = time.time()
project_files = {}
for file_path in files_path:
project_name = os.path.basename(file_path).split('_')[0]
if project_name not in project_files:
project_files[project_name] = []
project_files[project_name].append(file_path)
for project in project_files.iterkeys():
nb_versions = len(project_files[project])
if nb_versions > settings.MIN_PACKAGE_VERSIONS:
project_files[project] = sorted(project_files[project], key=lambda x: os.stat(x).st_mtime)
for filename in project_files[project]:
if (
nb_versions > settings.MIN_PACKAGE_VERSIONS
and os.stat(filename).st_mtime < now - settings.MIN_AGE * 86400
):
os.remove(filename)
nb_versions -= 1
def clean(method):
print('+ Cleanning %s' % method)
if method == 'all':
shutil.rmtree(settings.ORIGIN_PATH)
shutil.rmtree(settings.PBUILDER_RESULT)
shutil.rmtree(settings.GIT_PATH)
shutil.rmtree(settings.LOCK_PATH)
elif method == 'git':
shutil.rmtree(settings.GIT_PATH)
elif method == 'deb':
shutil.rmtree(settings.PBUILDER_RESULT)
elif method == 'archives':
shutil.rmtree(settings.ORIGIN_PATH)
elif method == 'locks':
shutil.rmtree(settings.LOCK_PATH)
elif method == 'smart':
results_files = []
origin_files = [os.path.join(settings.ORIGIN_PATH, f) for f in os.listdir(settings.ORIGIN_PATH)]
for root, dirs, files in os.walk(settings.PBUILDER_RESULT):
for fname in files:
results_files.append(os.path.join(root, fname))
smart_cleaning(results_files)
smart_cleaning(origin_files)
now = time.time()
for root, dirs, files in os.walk(settings.PBUILDER_RESULT):
for fname in files:
fname = os.path.join(root, fname)
ext = os.path.splitext(fname)
if ext == 'build' and os.stat(fname).st_mtime < now - 365 * 86400:
os.remove(fname)
else:
error("Cleanning: unknow '%s' option" % method)
def get_project_infos(git_project_path, cmd_options):
"""return a dict with project informations"""
os.chdir(git_project_path)
results = {
'name': '',
'version': '',
'fullname': '',
'ac_version': '',
'build_dir': '',
'build_branch': cmd_options.branch,
'commit_number': '',
'git_path': git_project_path,
}
if os.path.exists('setup.py'):
# Hack to support setup_requires
setup_py('--help')
results['name'] = setup_py('--name 2> /dev/null')[:-1]
results['version'] = setup_py('--version 2> /dev/null')[:-1]
results['fullname'] = setup_py('--fullname 2> /dev/null')[:-1]
elif os.path.exists('configure.ac'):
call('./autogen.sh')
call('make all')
results['name'] = output("./configure --version | head -n1 | sed 's/ configure.*//'")[:-1]
results['ac_version'] = output("./configure --version | head -n1 | sed 's/.* configure //'")[:-1]
results['version'] = results['ac_version'].replace('-', '.')
results['fullname'] = results['name']
elif os.path.exists('Makefile'):
results['name'] = output('make name')[:-1]
results['version'] = output('make version')[:-1]
results['fullname'] = output('make fullname')[:-1]
else:
error('Unsupported project type', exit_code=2)
results['build_dir'] = os.path.join(
settings.EOBUILDER_TMP, '%s-%d' % (results['name'], random.randint(0, 1000000))
)
atexit.register(rm_recursive, results['build_dir'])
results['commit_number'] = output('git rev-parse HEAD')[:-1]
results['lock_path'] = os.path.join(settings.LOCK_PATH, results['name'])
current_tag = output('git describe --abbrev=0 --tags --match=v*', exit_on_error=False)
if current_tag:
results['current_tag'] = current_tag[1:-1]
else:
results['current_tag'] = '0.0'
return results
def prepare_build(dist, project, cmd_options, new):
"""
Create origin archive, update git and Debian changelog
"""
package = {
'repository': settings.DEFAULT_UNSTABLE_REPOSITORIES[dist],
'version': '',
'source_name': '',
'names': [],
}
if cmd_options.hotfix:
package['repository'] = settings.HOTFIX_REPOSITORIES[dist]
os.chdir(project['git_path'])
build_branch = cmd_options.branch
if cmd_options.hotfix and not build_branch.startswith('hotfix/'):
return error('Invalid name for hotfix branch (must start with hotfix/)', exit_code=2)
debian_folder = cmd_options.debian_folder
if os.path.isdir('debian-' + dist) and debian_folder == 'debian':
debian_folder = 'debian-' + dist
debian_branch = None
if not os.path.isdir(debian_folder):
debian_branch = 'debian'
debian_folder = 'debian'
branches = output('git branch -r -l')
if debian_branch == 'debian' and 'debian-%s' % dist in branches:
debian_branch = 'debian-' + dist
if not 'origin/%s' % debian_branch in output('git branch -r -l'):
print('!!! WARNING: cannot build for dist %s, no debian directory found' % dist)
return
print('!!! WARNING obsolete: using a branch for debian/ packaging')
print('+ Updating Debian branch for %s' % dist)
call('git checkout --quiet %s' % debian_branch)
call('git pull')
else:
print('+ Building from %s debian folder' % debian_folder)
for r in cmd_options.repositories:
repo = r.split(':')
if repo[0] == dist:
package['repository'] = repo[1]
# get package source name
control_file = os.path.join(debian_folder, 'control')
package['names'] = re.findall(r'Package\s*:\s*(.*?)\n', cat(control_file))
package['source_name'] = re.search(r'^Source\s*:\s*(.*?)\n', cat(control_file), re.MULTILINE).group(1)
# build tarball
origin_archive = os.path.join(
settings.ORIGIN_PATH, '%s_%s.orig.tar.bz2' % (package['source_name'], project['version'])
)
if not os.path.exists(origin_archive):
print('+ Generating origin tarball ...')
os.chdir(project['git_path'])
call('git checkout --quiet %s' % build_branch)
if os.path.exists('setup.py'):
setup_py('clean --all')
setup_py('sdist --formats=bztar')
shutil.move('dist/%s.tar.bz2' % project['fullname'], origin_archive)
elif os.path.exists('./configure.ac'):
call('make dist-bzip2')
shutil.move('%s-%s.tar.bz2' % (project['name'], project['ac_version']), origin_archive)
elif os.path.exists('Makefile'):
call('make dist-bzip2')
shutil.move('sdist/%s.tar.bz2' % project['fullname'], origin_archive)
else:
error('Unsupported project type', project['build_dir'], exit_code=2)
last_version_file = os.path.join(
project['lock_path'],
'%s_%s_%s.last_version' % (project['name'], package['repository'], build_branch.replace('/', '_')),
)
debian_changelog = os.path.join(debian_folder, 'changelog')
if os.path.exists(last_version_file):
last_debian_package_version = cat(last_version_file)
else:
last_debian_package_version = re.search(
r'^Version:\s(.*?)$', output('dpkg-parsechangelog -l%s' % debian_changelog), re.MULTILINE
).group(1)
last_version = last_debian_package_version.split('-')[0]
package['version'] = last_debian_package_version
if cmd_options.native:
debian_revision_number = ''
else:
debian_revision_number = '-1'
if last_version == project['version'] and new and '~eob' in last_debian_package_version:
new_inc = int(last_debian_package_version.rsplit('+', 1)[-1]) + 1
version_suffix = '%s~eob%s+%s' % (debian_revision_number, settings.DEBIAN_VERSIONS[dist], new_inc)
else:
version_suffix = '%s~eob%s+1' % (debian_revision_number, settings.DEBIAN_VERSIONS[dist])
call('git checkout --quiet %s' % build_branch)
changelog = '\n'.join(
changelog_from_git(
package['source_name'],
version_suffix,
project['git_path'],
package['repository'],
epoch=cmd_options.epoch,
)
)
if changelog:
if not os.path.isdir(debian_folder):
call('git checkout --quiet %s' % debian_branch)
debian_generated_changelog_filename = debian_changelog + '.generated'
with open(debian_generated_changelog_filename, 'w+') as f:
f.write(changelog)
package['version'] = re.search(
r'^Version:\s(.*?)$',
output('dpkg-parsechangelog -l%s' % debian_generated_changelog_filename),
re.MULTILINE,
).group(1)
os.unlink(debian_generated_changelog_filename)
else:
# changelog couldn't be generated, this happens with checkouts from
# dgit, at least.
package['version'] = ''
if os.path.exists(project['build_dir']):
shutil.rmtree(project['build_dir'])
os.makedirs(project['build_dir'], 0o755)
if package['version'].split('-')[0].split(':')[-1] == project['version']:
# the generated changelog has the right version number, use it.
good_changelog_contents = changelog
else:
# wrong version number, in that case we add an arbitrary new entry
# to the existing changelog
package['version'] = '%s%s' % (project['version'], version_suffix)
call(
'dch "Eobuilder version" -v %s --distribution %s \
--force-bad-version --force-distribution --changelog %s'
% (
'%s:%s' % (cmd_options.epoch, package['version'])
if cmd_options.epoch
else package['version'],
package['repository'],
debian_changelog,
)
)
good_changelog_contents = open(debian_changelog).read()
if cmd_options.hotfix:
version_part = build_branch.split('/', 1)[1].lstrip('v')
if not project['version'].startswith(version_part):
return error('Invalid name for hotfix branch (must start with version number)', exit_code=2)
build_file = os.path.join(
project['lock_path'],
'%s_%s_%s_%s.build'
% (project['name'], package['version'], package['repository'], build_branch.replace('/', '_')),
)
if os.path.exists(build_file):
print('+ Already built for %s !' % dist)
return package
print('+ Preparing Debian build (%s %s) ...' % (package['source_name'], package['version']))
if debian_branch:
call('git checkout --quiet %s' % debian_branch)
os.chdir(project['build_dir'])
project_build_path = os.path.join(project['build_dir'], '%s-%s' % (project['name'], project['version']))
shutil.copy(origin_archive, project['build_dir'])
tar = tarfile.open('%s_%s.orig.tar.bz2' % (package['source_name'], project['version']), 'r:bz2')
tar.extractall()
tar.close()
if os.path.exists('%s/debian' % project_build_path):
shutil.rmtree('%s/debian' % project_build_path)
shutil.copytree(os.path.join(project['git_path'], debian_folder), '%s/debian' % project_build_path)
with open(os.path.join(project_build_path, 'debian', 'changelog'), 'w') as f:
f.write(good_changelog_contents)
return package
def build_project(dist, arch, project, package, new):
pbuilder_project_result = os.path.join(settings.PBUILDER_RESULT, '%s-%s' % (dist, arch))
project_build_path = os.path.join(project['build_dir'], '%s-%s' % (project['name'], project['version']))
if not os.path.exists(pbuilder_project_result):
os.makedirs(pbuilder_project_result, 0o755)
os.chdir(project['lock_path'])
source_build = os.path.join(
project['lock_path'],
'%s_%s_%s_%s_source.build'
% (
project['name'],
project['version'],
package['repository'],
project['build_branch'].replace('/', '_'),
),
)
bin_build = os.path.join(
project['lock_path'],
'%s_%s_%s_%s_%s.build'
% (
project['name'],
package['version'],
package['repository'],
project['build_branch'].replace('/', '_'),
arch,
),
)
print('SOURCE_BUILD:', source_build)
if os.path.exists(source_build):
source_opt = '-b'
else:
source_opt = '-sa'
if new == 0 and os.path.exists(bin_build):
print('+ Already build !')
return
os.chdir(project_build_path)
print('+ Building %s %s %s %s' % (project['name'], project['version'], dist, arch))
call(
'DIST=%s ARCH=%s pdebuild --use-pdebuild-internal --architecture %s --debbuildopts "%s"'
% (dist, arch, arch, source_opt)
)
print('+ Lock build')
touch(bin_build)
if not os.path.exists(source_build):
touch(source_build)
def send_packages(dist, arch, project, package, last_tag, dput=True):
stamp_file = os.path.join(
project['lock_path'],
'%s_%s_%s_%s_%s.upload'
% (
project['name'],
package['version'],
package['repository'],
arch,
project['build_branch'].replace('/', '_'),
),
)
if os.path.exists(stamp_file):
print('+ Already uploaded')
return
pbuilder_project_result = os.path.join(settings.PBUILDER_RESULT, '%s-%s' % (dist, arch))
print('+ Updating local repository...')
subprocess.check_call(
'apt-ftparchive packages . | gzip > Packages.gz', cwd=pbuilder_project_result, shell=True
)
if dput:
print('+ Sending package...')
os.chdir(pbuilder_project_result)
call(
'dput -u %s %s_%s_%s.changes'
% (package['repository'], package['source_name'], package['version'].split(':', 1)[-1], arch)
)
else:
print('+ Package not sent to repository (--no-dput used).')
return
open(stamp_file, 'w').close()
def clean_git_on_exit(git_project_path):
if not os.path.exists(git_project_path):
return
os.chdir(git_project_path)
call('git stash --quiet')
changelog_tmp = os.path.join(git_project_path, 'debian', 'changelog.git')
if os.path.exists(changelog_tmp):
os.remove(changelog_tmp)
def get_git_project_name(project_reference):
project_name = os.path.basename(project_reference)
if project_name.endswith('.git'):
project_name = project_name[:-4]
return project_name
def get_git_project_path(project_reference):
return os.path.join(settings.GIT_PATH, get_git_project_name(project_reference))
def get_git_branch_name(project_reference):
git_project_path = get_git_project_path(project_reference)
for branch_name in ('main', 'master'):
try:
subprocess.check_call(
['git', 'rev-parse', branch_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=git_project_path,
)
except subprocess.CalledProcessError:
continue
return branch_name
else:
raise Exception('failed to determine branch')
def setup_git_tree(project_reference, options):
git_project_path = get_git_project_path(project_reference)
if options.branch and options.branch.startswith('wip/'):
if os.path.exists(git_project_path):
shutil.rmtree(git_project_path)
if os.path.exists(git_project_path):
existing_tree = True
branch_name = options.branch or get_git_branch_name(project_reference)
try:
subprocess.check_call(['git', 'fetch'], cwd=git_project_path)
subprocess.check_call(['git', 'checkout', '--quiet', branch_name], cwd=git_project_path)
subprocess.check_call(['git', 'reset', '--hard', 'origin/%s' % branch_name], cwd=git_project_path)
except subprocess.CalledProcessError as e:
print(e, file=sys.stderr)
shutil.rmtree(git_project_path)
return setup_git_tree(project_reference, options)
else:
existing_tree = False
os.chdir(settings.GIT_PATH)
if project_reference.startswith('/'):
call('git clone %s' % project_reference)
else:
parsed = urllib.parse.urlparse(project_reference)
if not parsed.netloc:
project_url = urllib.parse.urljoin(settings.GIT_REPOSITORY_URL, project_reference) + '.git'
else:
project_url = project_reference
call('git clone %s' % project_url)
if options.branch:
subprocess.check_call(['git', 'checkout', '--quiet', options.branch], cwd=git_project_path)
branch_name = get_git_branch_name(project_reference)
if not options.branch:
options.branch = branch_name
os.chdir(git_project_path)
try:
subprocess.check_call(['git', 'checkout', '--quiet', branch_name])
subprocess.check_call(['git', 'pull'])
subprocess.check_call(['git', 'submodule', 'init'])
subprocess.check_call(['git', 'submodule', 'update'])
except subprocess.CalledProcessError as e:
if existing_tree:
print(e, file=sys.stderr)
shutil.rmtree(git_project_path)
return setup_git_tree(project_reference, options)
raise
def main():
options, args = parse_cmdline()
for method in options.cleaning:
clean(method)
if options.cleaning:
sys.exit(0)
init()
project_reference = args[0]
git_project_path = get_git_project_path(project_reference)
atexit.register(clean_git_on_exit, git_project_path)
if options.branch and options.branch.startswith('origin/'):
# normalize without origin/
options.branch = options.branch[len('origin/') :]
existing_tree = os.path.exists(git_project_path)
if existing_tree:
os.chdir(git_project_path)
last_tag = output('git describe --abbrev=0 --tags --match=v*', exit_on_error=False)
if last_tag:
last_tag = last_tag[1:-1]
else:
last_tag = '0.0'
else:
last_tag = '0.0'
setup_git_tree(project_reference, options)
project = get_project_infos(git_project_path, options)
if not os.path.exists(project['lock_path']):
os.mkdir(project['lock_path'], 0o755)
# compare revision between last build and now to determine if something is really new
new = 1
current_revision = output('git rev-parse HEAD', True).strip()
branch_name = get_git_branch_name(project_reference)
last_branch_revision_file_path = os.path.join(
project['lock_path'], '%s_%s.last_revision' % (project['name'], branch_name.replace('/', '_'))
)
try:
with open(last_branch_revision_file_path) as f:
last_branch_revision = f.read().strip()
except IOError:
pass
else:
if current_revision == last_branch_revision:
new = 0
if options.force and not new:
print('+ Warning force a new build')
new = 1
for dist in options.distrib:
os.chdir(git_project_path)
call('git checkout --quiet %s' % branch_name)
package = prepare_build(dist, project, options, new)
if package:
for arch in options.architectures:
build_project(dist, arch, project, package, new)
send_packages(dist, arch, project, package, last_tag, dput=options.dput)
print('+ Add a build file to lock new build for %s' % dist)
touch(
os.path.join(
project['lock_path'],
'%s_%s_%s_%s.build'
% (
project['name'],
package['version'],
package['repository'],
branch_name.replace('/', '_'),
),
)
)
last_version_file = os.path.join(
project['lock_path'],
'%s_%s_%s.last_version'
% (project['name'], package['repository'], branch_name.replace('/', '_')),
)
with open(last_version_file, 'w+') as f:
f.write(package['version'])
# keep current revision for next build
with open(last_branch_revision_file_path, 'w+') as f:
f.write(current_revision)
if __name__ == '__main__':
main()