1#!/usr/bin/env python3 2 3import sys 4import os 5import string 6import argparse 7import subprocess 8import fnmatch 9import pygit2 10import yaml 11 12def string_to_valid_file_name(to_convert): 13 valid_chars = '-_.()' + string.ascii_letters + string.digits 14 result = '' 15 for c in to_convert: 16 if c in valid_chars: 17 result += c 18 else: 19 result += '_' 20 # strip final dot, if any 21 if result.endswith('.'): 22 return result[:-1] 23 return result 24 25class wine_sync: 26 def __init__(self, module): 27 if os.path.isfile('winesync.cfg'): 28 with open('winesync.cfg', 'r') as file_input: 29 config = yaml.load(file_input, Loader=yaml.FullLoader) 30 self.reactos_src = config['repos']['reactos'] 31 self.wine_src = config['repos']['wine'] 32 self.wine_staging_src = config['repos']['wine-staging'] 33 else: 34 config = { } 35 self.reactos_src = input('Please enter the path to the reactos git tree: ') 36 self.wine_src = input('Please enter the path to the wine git tree: ') 37 self.wine_staging_src = input('Please enter the path to the wine-staging git tree: ') 38 config['repos'] = { 'reactos' : self.reactos_src, 39 'wine': self.wine_src, 40 'wine-staging': self.wine_staging_src } 41 with open('winesync.cfg', 'w') as file_output: 42 yaml.dump(config, file_output) 43 44 self.wine_repo = pygit2.Repository(self.wine_src) 45 self.wine_staging_repo = pygit2.Repository(self.wine_staging_src) 46 self.reactos_repo = pygit2.Repository(self.reactos_src) 47 48 # read the index from the reactos tree 49 self.reactos_index = self.reactos_repo.index 50 self.reactos_index.read() 51 52 # get the actual state for the asked module 53 self.module = module 54 with open(module + '.cfg', 'r') as file_input: 55 self.module_cfg = yaml.load(file_input, Loader=yaml.FullLoader) 56 57 self.staged_patch_dir = os.path.join('sdk', 'tools', 'winesync', self.module + '_staging') 58 59 def create_or_checkout_wine_branch(self, wine_tag, wine_staging_tag): 60 wine_branch_name = 'winesync-' + wine_tag + '-' + wine_staging_tag 61 branch = self.wine_repo.lookup_branch(wine_branch_name) 62 if branch is None: 63 # get our target commits 64 wine_target_commit = self.wine_repo.revparse_single(wine_tag) 65 if isinstance(wine_target_commit, pygit2.Tag): 66 wine_target_commit = wine_target_commit.target 67 68 wine_staging_target_commit = self.wine_staging_repo.revparse_single(wine_staging_tag) 69 if isinstance(wine_staging_target_commit, pygit2.Tag): 70 wine_staging_target_commit = wine_staging_target_commit.target 71 72 self.wine_repo.branches.local.create(wine_branch_name, self.wine_repo.revparse_single('HEAD')) 73 self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name)) 74 self.wine_repo.reset(wine_target_commit, pygit2.GIT_RESET_HARD) 75 76 # do the same for the wine-staging tree 77 self.wine_staging_repo.branches.local.create(wine_branch_name, self.wine_staging_repo.revparse_single('HEAD')) 78 self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch(wine_branch_name)) 79 self.wine_staging_repo.reset(wine_staging_target_commit, pygit2.GIT_RESET_HARD) 80 81 # run the wine-staging script 82 subprocess.call(['bash', '-c', self.wine_staging_src + '/patches/patchinstall.sh DESTDIR=' + self.wine_src + ' --all --backend=git-am']) 83 84 # delete the branch we created 85 self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch('master')) 86 self.wine_staging_repo.branches.delete(wine_branch_name) 87 else: 88 self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name)) 89 90 return wine_branch_name 91 92 # helper function for resolving wine tree path to reactos one 93 # Note : it doesn't care about the fact that the file actually exists or not 94 def wine_to_reactos_path(self, wine_path): 95 if wine_path in self.module_cfg['files']: 96 # we have a direct mapping 97 return self.module_cfg['files'][wine_path] 98 99 if not '/' in wine_path: 100 # root files should have a direct mapping 101 return None 102 103 wine_dir, wine_file = os.path.split(wine_path) 104 if wine_dir in self.module_cfg['directories']: 105 # we have a mapping for the directory 106 return os.path.join(self.module_cfg['directories'][wine_dir], wine_file) 107 108 # no match 109 return None 110 111 def sync_wine_commit(self, wine_commit, in_staging, staging_patch_index): 112 # Get the diff object 113 diff = self.wine_repo.diff(wine_commit.parents[0], wine_commit) 114 115 modified_files = [] 116 ignored_files = [] 117 warning_message = '' 118 complete_patch = '' 119 120 if in_staging: 121 # see if we already applied this patch 122 patch_file_name = f'{staging_patch_index:04}-{string_to_valid_file_name(wine_commit.message.splitlines()[0])}.diff' 123 patch_path = os.path.join(self.reactos_src, self.staged_patch_dir, patch_file_name) 124 if os.path.isfile(patch_path): 125 print(f'Skipping patch as {patch_path} already exists') 126 return True, '' 127 128 for delta in diff.deltas: 129 if delta.old_file.path == '/dev/null': 130 # check if we should care 131 new_reactos_path = self.wine_to_reactos_path(delta.new_file.path) 132 if not new_reactos_path is None: 133 warning_message += 'file ' + delta.new_file.path + ' is added to the wine tree !\n' 134 old_reactos_path = '/dev/null' 135 else: 136 old_reactos_path = None 137 elif delta.new_file.path == '/dev/null': 138 # check if we should care 139 old_reactos_path = self.wine_to_reactos_path(delta.old_file.path) 140 if not old_reactos_path is None: 141 warning_message += 'file ' + delta.old_file.path + ' is removed from the wine tree !\n' 142 new_reactos_path = '/dev/null' 143 else: 144 new_reactos_path = None 145 elif delta.new_file.path.endswith('Makefile.in'): 146 warning_message += 'file ' + delta.new_file.path + ' was modified !\n' 147 # no need to warn that those are ignored, we just did. 148 continue 149 else: 150 new_reactos_path = self.wine_to_reactos_path(delta.new_file.path) 151 old_reactos_path = self.wine_to_reactos_path(delta.old_file.path) 152 153 if (new_reactos_path is not None) or (old_reactos_path is not None): 154 # print('Must apply diff: ' + old_reactos_path + ' --> ' + new_reactos_path) 155 new_blob = self.wine_repo.get(wine_commit.tree[delta.new_file.path].id) 156 old_blob = self.wine_repo.get(wine_commit.parents[0].tree[delta.old_file.path].id) 157 158 blob_patch = pygit2.Patch.create_from( 159 old=old_blob, 160 new=new_blob, 161 old_as_path=old_reactos_path, 162 new_as_path=new_reactos_path) 163 164 # print(str(wine_commit.id)) 165 # print(blob_patch.text) 166 167 # this doesn't work 168 # reactos_diff = pygit2.Diff.parse_diff(blob_patch.text) 169 # reactos_repo.apply(reactos_diff) 170 try: 171 subprocess.run(['git', '-C', self.reactos_src, 'apply', '--reject'], input=blob_patch.data, check=True) 172 except subprocess.CalledProcessError as err: 173 warning_message += 'Error while applying patch to ' + new_reactos_path + '\n' 174 self.reactos_index.add(new_reactos_path) 175 176 complete_patch += blob_patch.text 177 178 modified_files += [delta.old_file.path, delta.new_file.path] 179 else: 180 ignored_files += [delta.old_file.path, delta.new_file.path] 181 182 if not modified_files: 183 # We applied nothing 184 return False, '' 185 186 print('Applied patches from wine commit ' + str(wine_commit.id)) 187 188 if ignored_files: 189 warning_message += 'WARNING : some files were ignored: ' + ' '.join(ignored_files) + '\n' 190 191 if not in_staging: 192 self.module_cfg['tags']['wine'] = str(wine_commit.id) 193 with open(self.module + '.cfg', 'w') as file_output: 194 yaml.dump(self.module_cfg, file_output) 195 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 196 else: 197 # Add the staging patch 198 # do not save the wine commit ID in <module>.cfg, as it's a local one for staging patches 199 if not os.path.isdir(os.path.join(self.reactos_src, self.staged_patch_dir)): 200 os.mkdir(os.path.join(self.reactos_src, self.staged_patch_dir)) 201 with open(patch_path, 'w') as file_output: 202 file_output.write(complete_patch) 203 self.reactos_index.add(os.path.join(self.staged_patch_dir, patch_file_name)) 204 205 self.reactos_index.write() 206 207 commit_msg = f'[WINESYNC] {wine_commit.message}\n' 208 if (in_staging): 209 commit_msg += f'wine-staging patch by {wine_commit.author.name} <{wine_commit.author.email}>' 210 else: 211 commit_msg += f'wine commit id {str(wine_commit.id)} by {wine_commit.author.name} <{wine_commit.author.email}>' 212 213 self.reactos_repo.create_commit('HEAD', 214 pygit2.Signature('winesync', 'ros-dev@reactos.org'), 215 self.reactos_repo.default_signature, 216 commit_msg, 217 self.reactos_index.write_tree(), 218 [self.reactos_repo.head.target]) 219 220 if (warning_message != ''): 221 warning_message += 'If needed, amend the current commit in your reactos tree and start this script again' 222 223 if not in_staging: 224 warning_message += f'You can see the details of the wine commit here: https://source.winehq.org/git/wine.git/commit/{str(wine_commit.id)}' 225 else: 226 warning_message += 'Do not forget to run\n' 227 warning_message += f'git diff HEAD^ \':(exclude)sdk/tools/winesync/{patch_file_name}\' > sdk/tools/winesync/{patch_file_name}\n' 228 warning_message += 'after your correction and then\n' 229 warning_message += f'git add sdk/tools/winesync/{patch_file_name}\n' 230 warning_message += 'before running "git commit --amend"' 231 232 return True, warning_message 233 234 def revert_staged_patchset(self): 235 # revert all of this in one commmit 236 staged_patch_dir_path = os.path.join(self.reactos_src, self.staged_patch_dir) 237 if not os.path.isdir(staged_patch_dir_path): 238 return True 239 240 has_patches = False 241 242 for patch_file_name in sorted(os.listdir(staged_patch_dir_path), reverse=True): 243 patch_path = os.path.join(staged_patch_dir_path, patch_file_name) 244 if not os.path.isfile(patch_path): 245 continue 246 247 has_patches = True 248 249 with open(patch_path, 'rb') as patch_file: 250 try: 251 subprocess.run(['git', '-C', self.reactos_src, 'apply', '-R', '--reject'], stdin=patch_file, check=True) 252 except subprocess.CalledProcessError as err: 253 print(f'Error while reverting patch {patch_file_name}') 254 print('Please check, remove the offending patch with git rm, and relaunch this script') 255 return False 256 257 self.reactos_index.remove(os.path.join(self.staged_patch_dir, patch_file_name)) 258 self.reactos_index.write() 259 os.remove(patch_path) 260 261 if not has_patches: 262 return True 263 264 self.reactos_index.add_all([f for f in self.module_cfg['files'].values()]) 265 self.reactos_index.add_all([f'{d}/*.*' for d in self.module_cfg['directories'].values()]) 266 self.reactos_index.write() 267 268 self.reactos_repo.create_commit( 269 'HEAD', 270 self.reactos_repo.default_signature, 271 self.reactos_repo.default_signature, 272 f'[WINESYNC]: revert wine-staging patchset for {self.module}', 273 self.reactos_index.write_tree(), 274 [self.reactos_repo.head.target]) 275 return True 276 277 def sync_to_wine(self, wine_tag, wine_staging_tag): 278 # Get our target commit 279 wine_target_commit = self.wine_repo.revparse_single(wine_tag) 280 if isinstance(wine_target_commit, pygit2.Tag): 281 wine_target_commit = wine_target_commit.target 282 # print(f'wine target commit is {wine_target_commit}') 283 284 # get the wine commit id where we left 285 in_staging = False 286 wine_last_sync = self.wine_repo.revparse_single(self.module_cfg['tags']['wine']) 287 if (isinstance(wine_last_sync, pygit2.Tag)): 288 if not self.revert_staged_patchset(): 289 return 290 wine_last_sync = wine_last_sync.target 291 if (isinstance(wine_last_sync, pygit2.Commit)): 292 wine_last_sync = wine_last_sync.id 293 294 # create a branch to keep things clean 295 wine_branch_name = self.create_or_checkout_wine_branch(wine_tag, wine_staging_tag) 296 297 finished_sync = True 298 staging_patch_index = 1 299 300 # walk each commit between last sync and the asked tag/revision 301 wine_commit_walker = self.wine_repo.walk(self.wine_repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_REVERSE) 302 wine_commit_walker.hide(wine_last_sync) 303 for wine_commit in wine_commit_walker: 304 applied_patch, warning_message = self.sync_wine_commit(wine_commit, in_staging, staging_patch_index) 305 306 if str(wine_commit.id) == str(wine_target_commit): 307 print('We are now in staging territory') 308 in_staging = True 309 310 if not applied_patch: 311 continue 312 313 if in_staging: 314 staging_patch_index += 1 315 316 if warning_message != '': 317 print(warning_message) 318 finished_sync = False 319 break 320 321 # we're done without error 322 if finished_sync: 323 # update wine tag and commit 324 self.module_cfg['tags']['wine'] = wine_tag 325 with open(self.module + '.cfg', 'w') as file_output: 326 yaml.dump(self.module_cfg, file_output) 327 328 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 329 self.reactos_index.write() 330 self.reactos_repo.create_commit( 331 'HEAD', 332 self.reactos_repo.default_signature, 333 self.reactos_repo.default_signature, 334 f'[WINESYNC]: {self.module} is now in sync with wine-staging {wine_tag}', 335 self.reactos_index.write_tree(), 336 [self.reactos_repo.head.target]) 337 338 print('The branch ' + wine_branch_name + ' was created in your wine repository. You might want to delete it, but you should keep it in case you want to sync more module up to this wine version') 339 340def main(): 341 parser = argparse.ArgumentParser() 342 parser.add_argument('module', help='The module you want to sync. <module>.cfg must exist in the current directory') 343 parser.add_argument('wine_tag', help='The wine tag or commit id to sync to') 344 parser.add_argument('wine_staging_tag', help='The wine staging tag or commit id to pick wine staged patches from') 345 346 args = parser.parse_args() 347 348 syncator = wine_sync(args.module) 349 350 return syncator.sync_to_wine(args.wine_tag, args.wine_staging_tag) 351 352 353if __name__ == '__main__': 354 main()