2 """ Rebuild AUR packages against newer dependencies
10 from srcinfo.parse import parse_srcinfo
11 from decimal import Decimal
12 #from pyalpm import vercmp
13 from xdg import BaseDirectory
15 # Get the path to user-specific cache files
16 ARGV0 = 'sync-rebuild'
17 cache_dir = BaseDirectory.xdg_cache_home
18 aurdest = os.path.join(cache_dir, 'aurutils/sync')
20 if 'AURDEST' in os.environ:
21 aurdest = os.getenv('AURDEST')
24 def run_readline(arg_list):
25 """Run the output from a command line-by-line.
27 `aur` programs typically use newline delimited output. Here, this function
28 is used with `aur repo` to read JSON objects, with each line representing
29 one local repository package.
32 with subprocess.Popen(arg_list, stdout=subprocess.PIPE) as process:
34 output = process.stdout.readline()
40 return_code = process.poll()
45 def srcinfo_get_version(srcinfo):
46 """Return the full version string from a .SRCINFO file.
48 The `epoch` key is optional, `pkgver` and `pkgrel` are assumed present.
51 with open(srcinfo, 'r', encoding='utf-8') as file:
52 (data, errors) = parse_srcinfo(file.read())
56 epoch = data.get('epoch')
57 pkgver = data['pkgver']
58 pkgrel = data['pkgrel']
61 return epoch + ':' + pkgver, pkgrel
65 def increase_decimal(decimal_number, increment, n_digits=2):
66 """Only increase the fractional part of a number.
68 # Convert the decimal number and increment to Decimal objects
69 decimal_num = Decimal(str(decimal_number))
70 inc = Decimal(str(increment))
72 # Calculate the increased decimal
73 increased_decimal = decimal_num + inc
75 # Convert the increased decimal to a formatted string with fixed precision
76 precision = '.' + str(n_digits) + 'f'
77 increased_decimal_str = format(increased_decimal, precision)
79 return increased_decimal_str
82 def update_pkgrel(buildscript, backup, pkgname, pkgrel=None, increment=0.1):
83 """Update pkgrel in a PKGBUILD by a given increment.
85 The original PKGBUILD is backed up before modification. Modifications assume
86 a single caller and are not thread-safe.
89 n_digits = sum(ch.isdigit() for ch in str(increment).strip('0'))
91 with fileinput.input(buildscript, inplace=True, backup=backup) as finput:
93 pkgrel_keyword = 'pkgrel='
95 if line.startswith(pkgrel_keyword):
96 # Extract and update the current pkgrel value
98 pkgrel = float(line.split('=')[1]) # Only the last written pkgrel holds
99 new_pkgrel = increase_decimal(pkgrel, increment, n_digits)
101 # Print bumped pkgrel to standard error
102 print(f'{ARGV0}: {pkgname}: {pkgrel} -> {new_pkgrel}', file=sys.stderr)
104 # Replace the pkgrel value in the line
105 line = f'{pkgrel_keyword}{new_pkgrel}\n'
107 # Write the modified line to stdout (which redirects to the PKGBUILD file)
111 # TODO: use vercmp to ensure rebuilds
112 # XXX: perform rebuilds in dependency order, abort reverse depends when depends fails (sync--ninja)
113 def rebuild_packages(repo_targets, db_name, fail_fast=False):
114 """Rebuild a series of packages in succession
116 # TODO: user-specified build arguments (e.g. --chroot)
117 build_cmd = ['aur', 'build', '-srn']
119 if db_name is not None:
120 build_cmd.extend(('--database', db_name))
122 # Check that `pkgver` is consistent between local repository and .SRCINFO
125 for pkgname, pkg in repo_targets.items():
126 # Only run once per pkgbase
127 if pkgname in rebuilds:
130 # Retrieve metdata from local repository entry
131 pkgbase = pkg['PackageBase']
132 pkgver, pkgrel = pkg['Version'].rsplit('-', 1)
134 # Retrieve metadata from .SRCINFO
135 src_dir = os.path.join(aurdest, pkgbase)
136 src_pkgver, _ = srcinfo_get_version(os.path.join(src_dir, '.SRCINFO'))
138 # Set backup file for PKGBUILD
139 buildscript = os.path.join(src_dir, 'PKGBUILD')
141 remove_backup = False
143 # Increase subrelease level to avoid conflicts with intermediate
145 if src_pkgver == pkgver:
146 update_pkgrel(buildscript, backup, pkgname, pkgrel=float(pkgrel), increment=0.1)
149 print(f'{ARGV0}: source and local repository version differ', file=sys.stderr)
150 print(f'{ARGV0}: using existing pkgver', file=sys.stderr)
154 # Build package with modified pkgrel
156 subprocess.run(build_cmd, check=True, cwd=src_dir)
158 # Build process completed successfully, remove backup PKGBUILD if it
161 os.remove(buildscript + backup)
163 except subprocess.CalledProcessError:
164 # Build process failed, revert to unmodified PKGBUILD
165 print(f'{ARGV0}: {pkgbase}: build failed, reverting PKGBUILD', file=sys.stderr)
166 os.replace(buildscript + backup, buildscript)
168 # --fail-fast: if a package failed to build, also consider remaining targets as failed
170 print(f'{ARGV0}: {pkgbase}: build failed, exiting', file=sys.stderr)
171 return rebuilds, list(set(repo_targets) - set(rebuilds))
173 # Mark rebuild as failure for later reporting to the user
174 failed_rebuilds[pkgname] = pkgbase
176 rebuilds[pkgname] = pkgbase
178 return rebuilds, failed_rebuilds
181 def print_cached_packages(args):
182 """Print cached packages in `vercmp` order
184 name_args = ['--name=' + item for item in args]
185 pacsift = ['pacsift', *name_args, '--exact', '--cache']
187 p1 = subprocess.Popen(pacsift, stdout=subprocess.PIPE)
188 p2 = subprocess.Popen(['pacsort'], stdin=p1.stdout, stderr=subprocess.PIPE)
189 p2.communicate() # wait for the pipeline to finish
192 def main(targets, db_name, fail_fast, run_sync):
193 # Ensure all sources are available. Only packages are cloned that are
194 # already available in the local repository.
195 sync_cmd = ['aur', 'sync', '--no-build', '--no-ver-argv']
196 repo_cmd = ['aur', 'repo', '--jsonl']
198 if db_name is not None:
199 sync_cmd.extend(('--database', db_name))
200 repo_cmd.extend(('--database', db_name))
204 # Read repository contents line by line to handle potentially large databases
205 for pkg_str in run_readline(repo_cmd):
206 pkg = json.loads(pkg_str)
207 pkgname = pkg['Name']
209 # Restrict to packages specified on the command-line
210 if pkgname in targets:
211 repo_targets[pkgname] = {
212 'PackageBase': pkg['PackageBase'], 'Version' : pkg['Version']
215 # Clone targets that are part of the local repository
216 if len(repo_targets) > 0:
217 sync_cmd.extend(list(repo_targets.keys()))
219 subprocess.run(sync_cmd, check=True)
221 print(f'{ARGV0}: no targets in local repository', file=sys.stderr)
224 rebuilds, failed = rebuild_packages(repo_targets, db_name, fail_fast)
227 print(f'{ARGV0}: the following targets failed to build:', file=sys.stderr)
228 print('\n'.join(rebuilds.keys()), file=sys.stderr)
230 rest = list(set(targets) - set(rebuilds.keys()) - set(failed.keys()))
233 print(f'{ARGV0}: the following targets are unavailable in the local repository',
235 print('\n'.join(rest), file=sys.stderr)
237 # Print any stale cached packages
238 print(f'\n{ARGV0}: with cached entries:', file=sys.stderr)
239 print_cached_packages(rest)
242 # Parse user arguments when run directly
243 if __name__ == '__main__':
245 parser = argparse.ArgumentParser(prog=f'{ARGV0}', description='rebuild packages')
246 parser.add_argument('-d', '--database')
247 parser.add_argument('--fail-fast', action='store_true')
248 parser.add_argument('--no-sync', action='store_false')
249 parser.add_argument('targets', nargs='+')
250 args = parser.parse_args()
252 main({i:1 for i in args.targets}, args.database, args.fail_fast, args.no_sync)