#!/usr/bin/env python2 # 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, tempfile 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` --no-patchlist do not generate a patch list (needed for `wine --patches`) --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 --no-patchlist -d, --destdir= install to (defaults to current working directory) ''' 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',patch]) elif backend == 'git-am-C1': return run(['git','-C',winedir,'am','-C1',patch]) elif backend == 'patch': with open(patch) as f: print patchdir+'/gitapply.sh <',patch return subprocess.call([patchdir+'/gitapply.sh'],stdin=f) elif backend == 'git-apply': return run(['git','-C',winedir,'apply','--index',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 generate_patchlist(patchlist): with tempfile.NamedTemporaryFile(mode='w') as f: f.write('''From: Wine Staging Team Subject: Autogenerated patch list. diff --git a/libs/wine/config.c b/libs/wine/config.c index 5262c76..0a3182f 100644 --- a/libs/wine/config.c +++ b/libs/wine/config.c @@ -478,10 +478,''' + str(21 + patch_data.count('\n')) + ''' @@ const char *wine_get_version(void) return PACKAGE_VERSION; } +static const struct +{ + const char *author; + const char *subject; + int revision; +} +wine_patch_data[] = +{ ''' + patch_data + '''+ {NULL, NULL, 0} +}; + /* return the applied non-standard patches */ const void *wine_get_patches(void) { - return NULL; + return &wine_patch_data[0]; } /* return the build id string */ ''') f.flush() apply_patch(f.name) def main(): global backend, force_autoconf, winedir patchlist = {} excluded = [] no_autoconf = False no_patchlist = 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', 'no-patchlist', '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 no_patchlist = 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 == '--no-patchlist': no_patchlist = True elif o == '--ignore-missing': ignore_missing = True for a in args: 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 'Staging' in patchlist and not no_patchlist: generate_patchlist(patchlist) 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()