#!/usr/bin/env python3 # Copyright (C) 2020 Zebediah Figura # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA # import email.header, getopt, os, subprocess, sys stagingdir = os.path.dirname(os.path.realpath(__file__)) + '/../' patchdir = stagingdir + 'patches' if 'DESTDIR' in os.environ: winedir = os.environ['DESTDIR'] else: winedir = '.' backend = "patch" force_autoconf = False applied = [] patch_data = '' def usage(): print(''' Usage: ./staging/patchinstall.py [options] [patchset ...] Applies all Wine-Staging patch sets, or each specified patch set plus its dependencies, to a Wine tree. Options: -h, --help print this message -v, --version print version information -a, --all apply all patches (except those excluded with -W) -W, --exclude exclude a patch --no-autoconf do not run autoreconf and tools/make_requests --force-autoconf run autoreconf and tools/make_requests after every patch (ignored when not using git-am or git-am-C1 backends) --backend= use the given backend to apply patches: patch use the `gitapply.sh` tool (a wrapper around `patch`) git-am use `git am` git-am-C1 use `git am -C1` git-apply use `git apply` --ignore-missing automatically add dependencies, but do not fail if they are missing or disabled -r, --rebase-mode alias for --backend=git-am-C1 --no-autoconf -d, --destdir= install to (defaults to current working directory) --upstream-commit print the Wine commit hash and exit ''') def run(*args, **kwargs): print(' '.join(args[0])) return subprocess.call(*args, **kwargs) # return a patch to be shoved in patchlist below def parse_def_file(name, path): deps = [] if os.path.exists(path): with open(path) as f: for l in f.readlines(): if l.lower().startswith('depends: '): deps.append(l.split(' ')[1].strip()) elif l.lower().strip() == 'disabled: true': return None return deps def apply_patch(patch): if backend == 'git-am': return run(['git','-C',winedir,'am','--whitespace=warn',patch]) elif backend == 'git-am-C1': return run(['git','-C',winedir,'am','--whitespace=warn','-C1',patch]) elif backend == 'patch': with open(patch) as f: print(patchdir+'/gitapply.sh -d', winedir, '<', patch) return subprocess.call([patchdir+'/gitapply.sh','-d',winedir],stdin=f) elif backend == 'git-apply': return run(['git','-C',winedir,'apply','--index','--whitespace=warn',patch]) def run_autoconf(patch): if not force_autoconf: return if backend != 'git-am' and backend != 'git-am-C1': print('Warning: ignoring --force-autoconf for backend ',backend) need_autoreconf = False need_make_requests = False with open(patch) as f: for line in f.readlines(): line = line.strip() if line == '--- a/configure.ac' or line == '--- a/aclocal.m4': need_autoreconf = True elif line == '--- a/server/protocol.def': need_make_requests = True if need_autoreconf: run(['autoreconf','-f'], cwd=winedir) if need_make_requests: run(['./tools/make_requests'], cwd=winedir) if need_autoreconf or need_make_requests: run(['git','-C',winedir,'commit','-a','--amend','--no-edit']) def add_patch_data(patch): global patch_data author = '' subject = '' with open(patch) as f: for line in f.readlines(): header = email.header.decode_header(line) if header[0][0] == 'From:': author = header[1][0] elif line[:5] == 'From:': author = line[6:line.index('<')-1] elif line[:8] == 'Subject:': subject = line[9:] if '[' in subject: subject = subject[subject.index(']') + 1:] if author and subject: break author = author.strip().strip('"').replace('\\','\\\\').replace('"','\\"') subject = subject.strip().replace('\\','\\\\').replace('"','\\"') patch_data += '+ {"%s", "%s", 1},\n' %(author, subject) def apply_set(patchlist, name): if name in applied: return True for dep in patchlist[name]: if dep in patchlist and not apply_set(patchlist, dep): return False for patch in sorted(os.listdir(patchdir+'/'+name)): if patch.endswith('.patch') and patch.startswith('0'): patch_file = patchdir + '/' + name + '/' + patch if apply_patch(patch_file): print('Failed to apply patch %s/%s' %(name, patch)) return False run_autoconf(patch_file) add_patch_data(patch_file) applied.append(name) return True def add_patchset(patchlist, name): path = patchdir + '/' + name if os.path.isdir(path): deps = parse_def_file(name, path + '/definition') if deps == None: print('Error: attempt to apply %s, but it is disabled.' %name) sys.exit(1) patchlist[name] = deps for dep in deps: add_patchset(patchlist, dep) def main(): global backend, force_autoconf, winedir patchlist = {} excluded = [] no_autoconf = False ignore_missing = False try: opts, args = getopt.gnu_getopt(sys.argv[1:], 'ad:hrvW:', \ ['all', 'backend=', 'destdir=', 'force-autoconf', 'ignore-missing', 'help', 'no-autoconf', 'upstream-commit', 'version']) except getopt.GetoptError as err: print(str(err)) sys.exit(2) for o, a in opts: if o == '-h' or o == '--help': usage() sys.exit(0) elif o == '-v' or o == '--version': with open(stagingdir + 'staging/VERSION') as f: print(f.read().rstrip()) print('wine-staging is distributed under the GNU LGPL 2.1.') print('See individual files for copyright information.') sys.exit(0) elif o == '-r' or o == '--rebase-mode': no_autoconf = True backend = "git-am-C1" elif o == '-W' or o == '--exclude': excluded.append(a) elif o == '-a' or o == '--all': for name in os.listdir(patchdir): path = patchdir + '/' + name if os.path.isdir(path): deps = parse_def_file(name, path + '/definition') if deps != None: # it's not disabled patchlist[name] = deps elif o == '-d' or o == '--destdir': winedir = a elif o == '--backend': backend = a elif o == '--no-autoconf': no_autoconf = True elif o == '--force-autoconf': force_autoconf = True elif o == '--ignore-missing': ignore_missing = True elif o == '--upstream-commit': with open(stagingdir + 'staging/upstream-commit') as f: print(f.read().rstrip()); sys.exit(0) for a in args: if a.startswith('DESTDIR='): winedir = a[8:] else: add_patchset(patchlist, a) for p in excluded: del patchlist[p] if not os.access(winedir + '/tools/make_requests', os.F_OK): print("Target directory '%s' does not point to a Wine tree." %winedir) sys.exit(1) if not patchlist: print('No patches specified, either use -a or specify one or more patch sets as arguments.') sys.exit(1) # Check that all of our dependencies exist for p in patchlist: deps = patchlist[p] for d in deps: if d not in patchlist: if not ignore_missing: print('Error: unknown or disabled dependency %s of %s.' %(d,p)) sys.exit(1) else: print('Warning: unknown or disabled dependency %s of %s.' %(d,p)) # Now try to apply each patch for p in sorted(patchlist.keys()): # Try to apply it if not apply_set(patchlist, p): sys.exit(1) if not no_autoconf and not force_autoconf: run(['autoreconf','-f'],cwd=winedir) run(['./tools/make_requests'],cwd=winedir) sys.exit(0) if __name__ == '__main__': main()