Skip to content

Commit

Permalink
merge packer.py and tagger.py
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanpoelen committed Jan 21, 2022
1 parent a9e15e7 commit b37dc91
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 159 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
__pycache__/
*.py[cod]

/debian

# C extensions
*.so

Expand Down
249 changes: 208 additions & 41 deletions packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,39 @@
# -*- coding: utf-8 -*-

import sys
import shutil
import os
import re
import shutil
import datetime
import subprocess
from collections.abc import Iterable

rgx_replace_var = re.compile('%[A-Z][A-Z0-9_]*%')
var_ident = '[A-Z][A-Z0-9_]*'

def replace_dict_all(text:str, dico:dict[str,str]) -> str:
result = []
rgx_replace_var = re.compile(f'%{var_ident}%')
text_parts = []
pos = 0
for m in rgx_replace_var.finditer(text):
result.append(text[pos:m.start()])
text_parts.append(text[pos:m.start()])
var = m.group()
result.append(dico.get(var[1:-1], var))
text_parts.append(dico.get(var[1:-1], var))
pos = m.end()
result.append(text[pos:])
return ''.join(result)
text_parts.append(text[pos:])
return ''.join(text_parts)

parse_config_rgx = re.compile('^\s*([^#][^=]*)=(.*)$')

def read_config(filename:str, configs:dict[str,str]=None) -> dict[str,str]:
def read_and_update_config(filename:str, configs:dict[str,str]=None) -> dict[str,str]:
""" Parse target Config files """
parse_config_rgx = re.compile(f'^({var_ident})\s*=(.*)')
configs = {} if configs is None else configs

with open(filename) as f:
for line in f:
if m := re.search(parse_config_rgx, line):
if line.startswith('include '):
directory = os.path.dirname(filename)
included_path = os.path.join(directory, line[8:].strip())
read_and_update_config(included_path, configs)
elif m := re.search(parse_config_rgx, line):
configs[m.group(1).strip()] = m.group(2).strip()

if not configs['PKG_DISTRIBUTION']:
Expand All @@ -43,6 +49,20 @@ def read_config(filename:str, configs:dict[str,str]=None) -> dict[str,str]:

return configs

def update_config_variables(config:dict[str,str], variables:Iterable[str]) -> list[str]:
"""Return unparsed variable"""
rgx_var = re.compile(f'^({var_ident})(\+?=)(.*)')
variable_errors = []
for var in variables:
m = rgx_var.match(var)
if m is None:
variable_errors.append(var)
continue
k = m.group(1)
newvalue = m.group(3) if m.group(2) == '=' else configs.get(k, '') + m.group(3)
configs[k] = newvalue
return variable_errors

def print_configs(configs:dict[str,str]) -> None:
variables = []
for k,v in configs.items():
Expand Down Expand Up @@ -82,14 +102,17 @@ def get_changelog_entry(project_name:str, version:str, maintainer:str, urgency:s
return (f'{project_name or "%PROJECT_NAME%"} ({version}%TARGET_NAME%) %PKG_DISTRIBUTION%; '
f'urgency={urgency}\n\n{"".join(changelog)}\n\n -- {maintainer} {now}\n\n')

def update_changelog(changelog_path:str, changelog:str):
def update_changelog(changelog_path:str, changelog:str) -> None:
changelog += readall(changelog_path)
writeall(changelog_path, changelog)

rgx_tempfile = re.compile('^#.*#$|~$')

def create_build_directory(package_template:str, output_build:str) -> None:
os.mkdir(output_build, 0o766)
rgx_tempfile = re.compile('^#.*#$|~$')

try:
os.mkdir(output_build, 0o766)
except FileExistsError:
pass

filenames = filter(lambda fname: rgx_tempfile.search(fname) is None,
os.listdir(package_template))
Expand All @@ -99,32 +122,165 @@ def create_build_directory(package_template:str, output_build:str) -> None:
out = replace_dict_all(out, configs)
writeall(f'{output_build}/{filename}', out)

def less_version(lhs_version:str, rhs_version:str) -> bool:
rgx_split_version = re.compile('[^\d]*(\d+)(?:.(\d+))?(?:.(\d+))?(?:.(\d+))?(?:.(\d+))?(.*)')
to_tuple = lambda m: (
int(m.group(1)),
int(m.group(2) or 0),
int(m.group(3) or 0),
int(m.group(4) or 0),
int(m.group(5) or 0),
m.group(6)
)
m1 = rgx_split_version.match(lhs_version)
m2 = rgx_split_version.match(rhs_version)
return to_tuple(m1) < to_tuple(m2)

def shell_cmd(cmd:list[str]) -> str:
print('$', ' '.join(cmd))
return subprocess.check_output(cmd, env={'GIT_PAGER':''}, text=True)

def git_uncommited_changes() -> str:
return shell_cmd(['git', 'diff', '--shortstat'])

def git_tag_exists(tag:str) -> tuple[bool, str]:
tag = tag+'\n'

# local tag
tags = shell_cmd(['git', 'tag', '--list'])
if tag in tags:
return (True, 'local')

# remote tag
tags = shell_cmd(['git', 'ls-remote', '--tags', 'origin'])
if '/' + tag in tags:
return (True, 'remote')

return (False, '')

def git_last_tag() -> str:
return shell_cmd(['git', 'describe', '--tags']).partition('\n')[0]

def regex_version_or_die(pattern:str, errcode:int) -> re.Pattern:
try:
return re.compile(pattern)
except re.error as e:
print('Regex version:', e, file=sys.stderr)
exit(errcode)

def read_version_or_die(pattern:str, file_version:str, errcode:int) -> tuple[str, str]:
if not file_version:
print('File version is empty', file=sys.stderr)
exit(errcode)

rgx_version = regex_version_or_die(pattern, 1)
content = readall(file_version)

m = rgx_version.match(content)

if m is None:
print(f'Version not found (pattern is {pattern})', file=sys.stderr)
exit(errcode)

return m.group(1), content


if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser(description='Packager for proxies repositories')
parser.add_argument('target', metavar='PATH', type=str, help='target file')
parser.add_argument('--build', metavar='DIRNAME', type=str, default='debian', help='build directory')
parser.add_argument('--clean-build', action='store_true', help='remove existing build directory')
parser.add_argument('--package-template', metavar='DIRNAME', type=str,
default='packaging/template/debian', help='package template directory')
parser.add_argument('--build-package', action='store_true', help='run dpkg-buildpackage')
parser.add_argument('--project-name', metavar='NAME', type=str)
parser.add_argument('--project-version', metavar='VERSION', type=str)
parser.add_argument('--update-changelog', action='store_true')
parser.add_argument('--urgency', type=str, default='low')
parser.add_argument('--utc', type=str, default='0200')
parser.add_argument('--maintainer', type=str, default='Proxies Team <R&[email protected]>')
parser.add_argument('--distribution-name')
parser.add_argument('--distribution-version')
parser.add_argument('--distribution-id')
parser.add_argument('--package-distribution')
parser.add_argument('-s', '--variable', metavar='VARIABLE=VALUE', action='append', default=[])
parser.add_argument('--show-config', action='store_true', help='show configs')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-b', '--build', action='store_true', help='create a package directory')
group.add_argument('-g', '--get-version', action='store_true')
group.add_argument('-n', '--new-version', metavar='VERSION', help='update version and create a tag')
group.add_argument('--show-config', action='store_true', help='show configs')

parser.add_argument('--no-check-uncommited', action='store_true')
parser.add_argument('--check-uncommited', dest='no_check_uncommited', action='store_false')

group = parser.add_argument_group('Version options')
group.add_argument('--file-version', metavar='PATH')
group.add_argument('--pattern-version', metavar='REGEX',
default='\s*(?:[a-zA-Z_][a-zA-Z0-9_]*)?VERSION\s*=\s*[\'"]([^\'"]*)[\'"]',
help='pattern for version')

group = parser.add_argument_group('Update version and tag')
group.add_argument('--force-version', action='store_true')
group.add_argument('--no-commit-and-tag', action='store_true')
group.add_argument('--commit-and-tag', dest='no_commit_and_tag', action='store_false')
group.add_argument('--no-push', action='store_true')
group.add_argument('--push', dest='no_push', action='store_false')
group.add_argument('--commit-message', metavar='TEMPLATE', default='Version %s', action='store_false',
help='commit message template, %s is replaced with new version')

group = parser.add_argument_group('Build package options')
group.add_argument('--target', metavar='NAME', help='target file path')
group.add_argument('--output-build', metavar='DIRNAME', default='debian', help='build directory')
group.add_argument('--clean-build', action='store_true', help='remove existing build directory')
group.add_argument('--package-template', metavar='DIRNAME',
default='packaging/template/debian', help='package template directory')
group.add_argument('--build-package', action='store_true', help='run dpkg-buildpackage')
group.add_argument('--project-name', metavar='NAME')
group.add_argument('--project-version', metavar='VERSION')
group.add_argument('--update-changelog', action='store_true')
group.add_argument('--urgency', default='low')
group.add_argument('--utc', default='0200')
group.add_argument('--maintainer', default='Proxies Team <R&[email protected]>')
group.add_argument('--distribution-name')
group.add_argument('--distribution-version')
group.add_argument('--distribution-id')
group.add_argument('--package-distribution')
group.add_argument('--no-check-version', action='store_true')
group.add_argument('--check-version', dest='no_check_version', action='store_false')
group.add_argument('-s', '--variable', metavar='VARIABLE=VALUE', action='append', default=[],
help='support of VARIABLE=VALUE and VARIABLE+=VALUE')

args = parser.parse_args()

if not args.show_config and not args.get_version and not args.no_check_uncommited:
changes = git_uncommited_changes()
if changes:
print(f'Your repository has uncommited changes:\n{changes}\n'
'Please commit before packaging or use --no-check-uncommited', file=sys.stderr)
exit(11)

# Version
new_version = args.new_version
if args.get_version or new_version is not None:
version, content = read_version_or_die(args.pattern_version, args.file_version, 1)

if args.get_version:
print(version)
exit(0)

if not new_version:
print(f'New version is empty', file=sys.stderr)
exit(3)

if not args.force_version and not less_version(version, new_version):
print(f'New version ({new_version}) is less than old version ({version})', file=sys.stderr)
exit(4)

content = f'{content[:m.start(1)]}{new_version}{content[m.end(1):]}'

tag_exists, tag_cat = git_tag_exists(new_version)
if tag_exists:
print(f'Tag {new_version} already exists ({tag_cat})', file=sys.stderr)
exit(4)

if not args.no_commit_and_tag:
shell_cmd(['git', 'commit', '-am', args.commit_message % (new_version,)])
shell_cmd(['git', 'tag', new_version])
if not args.no_push:
shell_cmd(['git', 'push'])
shell_cmd(['git', 'push', '--tags'])

if not args.target:
print('--target is missing', file=sys.stderr)
exit(1)

# Build / Show config
if not args.distribution_name or not args.distribution_version or not args.distribution_id:
import distro
args.distribution_id = args.distribution_id or distro.name()
Expand All @@ -145,9 +301,13 @@ def create_build_directory(package_template:str, output_build:str) -> None:
'UTC': args.utc or '0200',
}

read_config(args.target, configs)
read_and_update_config(args.target, configs)

configs.update(variable.partition('=')[0::2] for variable in args.variable)
variable_errors = update_config_variables(configs, args.variable)
if variable_errors:
print('Parse error on -s / --variable: "', '", "'.join(variable_errors), '"',
sep='', file=sys.stderr)
exit(20)

if args.show_config:
print_configs(configs)
Expand All @@ -156,14 +316,21 @@ def create_build_directory(package_template:str, output_build:str) -> None:
project_version = configs['PROJECT_VERSION']
project_name = configs['PROJECT_NAME']

if not args.no_check_version or not project_version:
version, _ = read_version_or_die(args.pattern_version, args.file_version, 3)
if not args.no_check_version and (project_version != version or project_version != git_last_tag()):
print('Repository head mismatch current version. Ignored with --no-check-version', file=sys.stderr)
exit(4)

if not project_version:
project_version = version

if not project_version or not project_name:
if not project_version:
print('--project-version or -s PROJECT_VERSION=... is missing', file=sys.stderr)
if not project_name:
print('--project-name or -s PROJECT_NAME=... is missing', file=sys.stderr)
print(file=sys.stderr)
parser.print_help()
exit(1)
exit(2)

if args.update_changelog:
changelog = get_changelog_entry(project_name,
Expand All @@ -175,14 +342,14 @@ def create_build_directory(package_template:str, output_build:str) -> None:

if args.clean_build:
try:
shutil.rmtree(args.build)
shutil.rmtree(args.output_build)
except:
pass

create_build_directory(args.package_template, args.build)
create_build_directory(args.package_template, args.output_build)

if args.build_package:
status = os.system('dpkg-buildpackage -b -tc -us -uc -r')
if status:
print('Build failed: dpkg-buildpackage error', file=sys.stderr)
exit(2)
exit(5)
2 changes: 1 addition & 1 deletion packaging/targets/debian
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ PROJECT_NAME=packager
MAINTAINER=WAB Dev Team <[email protected]>
VCS_GIT=https://github.com/wallix/packager.git
VCS_BROWSER=https://github.com/wallix/packager
HOMEPAGE=https://github.com/wallix/packager
HOMEPAGE=https://github.com/wallix/packager
Loading

0 comments on commit b37dc91

Please sign in to comment.