3 # Copyright (c) 2007-2009 Kimmo Varis
4 # Copyright (c) 2008 Matthias Mayer
6 # Permission is hereby granted, free of charge, to any person obtaining
7 # a copy of this software and associated documentation files
8 # (the "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject to
12 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 # This is a script for creating a WinMerge release.
27 # - cleans previous build files from folders
28 # - sets version number for resources
29 # - updates POT and PO files
30 # - builds libraries (expat, scew, pcre)
31 # - builds WinMerge.exe and WinMergeU.exe
32 # - builds 32-bit ShellExtension targets
33 # - builds user manual
34 # - builds the InnoSetup installer
35 # - creates per-version distribution folder
36 # - exports SVN sources to distribution folder
37 # - creates binary distribution folder
39 #Tasks not done (TODO?):
40 # - building 64-bit ShellExtension
41 # - creating packages from source and binary folders
42 # - running virus check
43 # - creating SHA-1 hashes for distributed files
44 # - create installer to correct folder
45 # - make installer compile less verbose
46 # - make building exes and dlls also less verbose
47 # - builds libraries twice: as independent project and from executable project (by prelink.bat)
48 # - check Python version
49 # - check JRE is installed
50 # - check InnoSetup is installed
53 # - Visual Studio 2003 or later
54 # - Python 2.5 or 2.6 :)
55 # - Subversion command line binaries from http://subversion.tigris.org/
57 # - Manual build tools (in Developer tools at downloads)
58 # - Java runtime environment (JRE) (Manual build tools need it)
59 # There are lots of other dependencies, they are documented in /Docs/Developers documents. Especially be sure to read:
60 # - Compiling.html for compiling executables and dlls
61 # - readme-manual.html for building the manual
62 # - readme-InnoSetup.html for creating the installer
64 # Please note that this script is only tested in kimmov's environment. It simply may not work in other environments and
65 # configurations. If you find problems, please report them so we can improve the script.
68 # Set these variables to match your environment and folders you want to use
70 # Subversion binary - set this to absolute path to svn.exe
71 #svn_binary = r'C:\Program Files\Subversion\bin\svn.exe'
73 #vs_path = r'C:\Program Files\Microsoft Visual Studio .NET 2003'
74 # InnoSetup installation path
75 #innosetup_path = 'C:\\Program Files\\Inno Setup 5'
76 # Relative path where to create a release folder
77 dist_root_folder = 'distrib'
79 # Give URL to SVN repository to export source from SVN or 'workspace' to export
81 #source_location = 'https://winmerge.svn.sourceforge.net/svnroot/winmerge/trunk'
82 #source_location ='workspace'
84 # END CONFIGURATION - you don't need to edit anything below...
86 from subprocess import *
98 self.svn_binary = r'C:\Program Files\Subversion\bin\svn.exe'
100 self.vs_path7 = r'C:\Program Files\Microsoft Visual Studio .NET 2003'
101 self.vs_path8 = r'C:\Program Files\Microsoft Visual Studio 8.0'
102 self.vs_path9 = r'C:\Program Files\Microsoft Visual Studio 9.0'
103 self.innosetup_path = r'C:\Program Files\Inno Setup 5'
104 self.winmerge_iss = 'WinMerge.iss' #filename only
105 self.winmerge_iss_path = 'WinMerge.iss' #including path
106 self.source = 'workspace'
108 self.vs_version = 2003
110 def create_ini(self, filename):
111 config = ConfigParser.RawConfigParser()
113 if os.path.exists('Tools.ini'):
114 config.readfp(open(filename))
115 if not config.has_section(sect):
116 config.add_section(sect)
117 if not config.has_option(sect, 'type'):
118 config.set(sect, 'type', 'VSXXXX')
119 if not config.has_option(sect, 'VSStudio'):
120 config.set(sect, 'VSStudio', self.vs_version)
121 if not config.has_option(sect, 'Source'):
122 config.set(sect, 'Source', self.source)
123 if not config.has_option(sect, 'svn_binary'):
124 config.set(sect, 'svn_binary', self.svn_binary)
125 if not config.has_option(sect, 'vs_path7'):
126 config.set(sect, 'vs_path7', self.vs_path7)
127 if not config.has_option(sect, 'vs_path8'):
128 config.set(sect, 'vs_path8', self.vs_path8)
129 if not config.has_option(sect, 'vs_path9'):
130 config.set(sect, 'vs_path9', self.vs_path9)
131 if not config.has_option(sect, 'innosetup_path'):
132 config.set(sect, 'innosetup_path', self.innosetup_path)
134 # Writing our configuration file to 'Tools.ini'
135 with open(filename, 'w') as configfile:
136 config.write(configfile)
138 def read_ini(self, filename):
139 config = ConfigParser.RawConfigParser()
140 if not os.path.exists(filename):
141 # If the config file didn't exist, we create a new file and ask
142 # user to edit the config and re-run the script. This is because
143 # our defaults probably don't match user's environment.
144 self.create_ini(filename)
145 print 'New configuration file created: ' + filename
146 print 'Please edit the file to match your configuration and re-run the script.'
149 config.readfp(open(filename))
150 self.svn_binary = config.get('RUNTIME', 'svn_binary')
151 self.vs_path7 = config.get('RUNTIME', 'vs_path7')
152 self.vs_path8 = config.get('RUNTIME', 'vs_path8')
153 self.vs_path9 = config.get('RUNTIME', 'vs_path9')
154 self.innosetup_path = config.get('RUNTIME', 'innosetup_path')
155 self.source = config.get('RUNTIME', 'Source')
156 self.vs_version = config.getint('RUNTIME', 'VSStudio')
158 if self.vs_version ==2003:
159 self.vs_path =self.vs_path7
160 elif self.vs_version ==2005:
161 self.vs_path =self.vs_path8
162 elif self.vs_version ==2008:
163 self.vs_path =self.vs_path9
165 # global settings class instance
168 def get_vs_ide_bin():
169 """Gets a full path to the Visual Studio IDE executable to run."""
171 # These are identical for VS2003.Net, VS2005 and VS2008
172 rel_path = 'Common7/IDE'
173 vs_bin = 'devenv.com'
175 vs_ide_path = os.path.join(prog.vs_path, rel_path)
176 vs_cmd_path = os.path.join(vs_ide_path, vs_bin)
180 """Deletes all build files around folder structure"""
182 print 'Delete old build files...'
183 winmerge_temp = 'BuildTmp'
184 if os.path.exists(winmerge_temp):
185 print 'Remove folder %s' % winmerge_temp
186 shutil.rmtree(winmerge_temp, True)
188 print 'Skipping folder %s' % winmerge_temp
191 print 'Remove ANSI files'
192 if os.path.exists('build/mergerelease/WinMerge.exe'):
193 os.remove('build/mergerelease/WinMerge.exe')
194 if os.path.exists('build/mergerelease/ShellExtension.dll'):
195 os.remove('build/mergerelease/ShellExtension.dll')
196 if os.path.exists('build/mergerelease/MergeLang.dll'):
197 os.remove('build/mergerelease/MergeLang.dll')
199 print 'Remove Unicode files'
200 if os.path.exists('build/mergeunicoderelease/WinMergeU.exe'):
201 os.remove('build/mergeunicoderelease/WinMergeU.exe')
202 if os.path.exists('build/mergeunicoderelease/ShellExtensionU.dll'):
203 os.remove('build/mergeunicoderelease/ShellExtensionU.dll')
204 if os.path.exists('build/mergeunicoderelease/MergeLang.dll'):
205 os.remove('build/mergeunicoderelease/MergeLang.dll')
207 print 'Remove expat files'
208 if os.path.exists('build/expat'):
209 shutil.rmtree('build/expat', True)
210 if os.path.exists('build/mergerelease/libexpat.dll'):
211 os.remove('build/mergerelease/libexpat.dll')
212 if os.path.exists('build/mergeunicoderelease/libexpat.dll'):
213 os.remove('build/mergeunicoderelease/libexpat.dll')
215 print 'Remove pcre files'
216 if os.path.exists('build/pcre'):
217 shutil.rmtree('build/pcre', True)
218 if os.path.exists('build/mergerelease/pcre.dll'):
219 os.remove('build/mergerelease/pcre.dll')
220 if os.path.exists('build/mergeunicoderelease/pcre.dll'):
221 os.remove('build/mergeunicoderelease/pcre.dll')
223 if os.path.exists('build/scew'):
224 shutil.rmtree('build/scew', True)
226 if os.path.exists('build/Manual'):
227 shutil.rmtree('build/Manual',True)
229 except EnvironmentError, einst:
230 print 'Error deleting files: '
234 print 'Error deleting files: '
235 print sys.exc_info()[0]
239 def get_product_version(file):
240 """Get the product version number from config file."""
242 version = SetVersions.get_product_version(file)
245 def set_resource_version(file):
246 """Sets the version number to the resource."""
248 print 'Update version number to resource(s)...'
249 SetVersions.process_versions(file)
251 def setup_translations():
252 """Updates translation files by running scripts in Src/Languages."""
254 # Scripts must be run from the directory where they reside
256 os.chdir('Translations/WinMerge')
257 call(['cscript', '/nologo', 'CreateMasterPotFile.vbs'])
258 call(['cscript', '/nologo', 'UpdatePoFilesFromPotFile.vbs'])
261 def get_and_create_dist_folder(folder):
262 """Formats a folder name for version-specific distribution folder
263 and creates the folder."""
265 abs_folder = os.path.realpath(dist_root_folder)
266 dist_folder = os.path.join(abs_folder, folder)
267 if os.path.exists(dist_folder):
268 print 'Folder: ' + dist_folder + ' already exists!'
269 print 'If you want to re-create this version, remove folder first!'
272 print 'Create distribution folder: ' + dist_folder
273 os.mkdir(dist_folder)
276 def get_src_dist_folder(dist_folder, folder):
277 """Format a source distribution folder path."""
279 dist_src = os.path.join(dist_folder, folder + '-src')
282 def svn_export(dist_src_folder):
283 """Exports sources to distribution folder."""
285 print 'Exporting sources to ' + dist_src_folder
286 print 'Exporting from: ' + prog.source
287 if prog.source == 'workspace':
288 call([prog.svn_binary, 'export', '--non-interactive', '.', dist_src_folder])
290 call([prog.svn_binary, 'export', '--non-interactive', source_location, dist_src_folder])
292 def cleanup_dlls_from_plugins(dist_src_folder):
293 """Remove compiled plugin dll files from source distribution folders."""
295 dll_folder = os.path.join(dist_src_folder, 'Plugins/dlls')
296 files = os.listdir(dll_folder)
298 print 'Removing dll files from plugin folder...'
299 for cur_file in files:
300 fullpath = os.path.join(dll_folder, cur_file)
301 if os.path.isfile(fullpath):
302 file_name, file_ext = os.path.splitext(cur_file)
303 if (file_ext == '.dll'):
306 def build_libraries():
307 """Builds library targets: expat, scew and pcre."""
309 vs_cmd = get_vs_ide_bin()
310 cur_path = os.getcwd()
312 print 'Build expat library...'
313 solution_path = os.path.join(cur_path, 'Externals/expat/lib/expat.vcproj')
315 call([vs_cmd, solution_path, '/rebuild', 'Release'], shell=True)
317 print 'Build scew library...'
318 solution_path = os.path.join(cur_path, 'Externals/scew/win32/scew.vcproj')
320 call([vs_cmd, solution_path, '/rebuild', 'Release'], shell=True)
322 print 'Build pcre library...'
323 solution_path = os.path.join(cur_path, 'Externals/pcre/Win32/pcre.vcproj')
325 call([vs_cmd, solution_path, '/rebuild', 'MinSizeRel'], shell=True)
328 """Builds all WinMerge targets."""
332 vs_cmd = get_vs_ide_bin()
334 build_winmerge(vs_cmd)
335 build_shellext(vs_cmd)
337 def build_winmerge(vs_cmd):
338 """Builds WinMerge executable targets."""
340 cur_path = os.getcwd()
341 solution_path = os.path.join(cur_path, 'Src\\Merge.vcproj')
344 # devenv Src\Merge.dsp /rebuild Release
345 print 'Build WinMerge executables...'
346 call([vs_cmd, solution_path, '/rebuild', 'Release'], shell=True)
347 call([vs_cmd, solution_path, '/rebuild', 'UnicodeRelease'], shell=True)
349 def build_shellext(vs_cmd):
350 """Builds 32-bit ShellExtension."""
352 cur_path = os.getcwd()
353 solution_path = os.path.join(cur_path, 'ShellExtension\\ShellExtension.vcproj')
355 # devenv Src\Merge.dsp /rebuild Release
356 print 'Build ShellExtension dlls...'
357 call([vs_cmd, solution_path, '/rebuild', 'Release MinDependency'])
358 call([vs_cmd, solution_path, '/rebuild', 'Unicode Release MinDependency'])
361 """Builds manual's HTML Help (CHM) version for user install and
362 HTML version for the Web. HTML version is created with ads."""
365 os.chdir('Docs/Users/Manual/build')
366 print 'Build HTML Help (CHM) manual...'
367 call(['build_htmlhelp.bat'])
369 # HTML manual not build in trunk.
370 #print 'Build HTML manual for Web with ads...'
371 #call(['build_html.bat', 'withads'])
372 print 'Manual build finished.'
375 def build_innosetup_installer(target_folder):
376 """Builds the InnoSetup installer for the WinMerge."""
378 innosetup_exe = os.path.join(prog.innosetup_path, 'iscc.exe')
379 cur_path = os.getcwd()
381 prog.winmerge_iss_path = os.path.join(cur_path, 'Installer\\InnoSetup\\' + prog.winmerge_iss)
383 #output_switch = '/O"' + target_folder + '"'
385 print 'Build Innosetup installer...'
386 # Should be able to give folder for created file and Q switch to make build quiet
387 #call([innosetup_exe, '/Q', output_switch, winmerge_iss])
388 call([innosetup_exe, prog.winmerge_iss_path])
390 def get_and_create_bin_folder(dist_folder, folder):
391 """Formats and creates binary distribution folder."""
393 bin_folder = os.path.join(dist_folder, folder + '-exe')
394 print 'Create binary distribution folder: ' + bin_folder
398 def create_bin_folders(bin_folder, dist_src_folder):
399 """Creates binary distribution folders."""
401 cur_path = os.getcwd()
403 print 'Create binary distribution folder structure...'
404 lang_folder = os.path.join(bin_folder, 'Languages')
405 os.mkdir(lang_folder)
406 doc_folder = os.path.join(bin_folder, 'Docs')
408 filters_folder = os.path.join(bin_folder, 'Filters')
409 plugins_folder = os.path.join(bin_folder, 'MergePlugins')
412 print 'Copying files to binary distribution folder...'
413 shutil.copy('build/mergerelease/WinMerge.exe', bin_folder)
414 shutil.copy('build/mergeunicoderelease/WinMergeU.exe', bin_folder)
416 shutil.copy('build/mergerelease/ShellExtension.dll', bin_folder)
417 shutil.copy('build/mergeunicoderelease/ShellExtensionU.dll', bin_folder)
418 shutil.copy('build/mergeunicoderelease/MergeLang.dll', bin_folder)
419 shutil.copy('build/shellextensionx64/ShellExtensionX64.dll', bin_folder)
420 shutil.copy('ShellExtension/Register.bat', bin_folder)
421 shutil.copy('ShellExtension/UnRegister.bat', bin_folder)
423 shutil.copy('build/pcre/pcre.dll', bin_folder)
424 shutil.copy('build/expat/libexpat.dll', bin_folder)
426 copy_po_files(lang_folder)
427 filter_orig = os.path.join(dist_src_folder, 'Filters')
428 shutil.copytree(filter_orig, filters_folder, False)
430 # Copy compiled plugins dir and rename it
431 plugin_dir = os.path.join(bin_folder, 'dlls')
432 plugin_orig = os.path.join(dist_src_folder, 'Plugins/dlls')
433 shutil.copytree(plugin_orig, plugin_dir, False)
434 os.rename(plugin_dir, plugins_folder)
436 shutil.copy('build/Manual/htmlhelp/WinMerge.chm', doc_folder)
438 shutil.copy('Docs/Users/ReleaseNotes.html', doc_folder)
439 shutil.copy('Docs/Users/ReadMe.txt', bin_folder)
440 shutil.copy('Docs/Users/ChangeLog.txt', doc_folder)
441 shutil.copy('Docs/Users/Contributors.txt', bin_folder)
442 shutil.copy('Docs/Users/Files.txt', bin_folder)
444 def copy_po_files(dest_folder):
445 """Copies all PO files to destination folder."""
447 lang_folder = 'Translations/WinMerge'
448 files = os.listdir(lang_folder)
450 print 'Copying PO files to binary folder...'
451 for cur_file in files:
452 fullpath = os.path.join(lang_folder, cur_file)
453 if os.path.isfile(fullpath):
454 file_name, file_ext = os.path.splitext(cur_file)
455 if (file_ext == '.po'):
456 shutil.copy(fullpath, dest_folder)
458 def find_winmerge_root():
459 """Find WinMerge tree root folder from where to run rest of the script.
461 This function checks if we are in WinMerge root folder. If we are in some
462 other folder then we must try to find the WinMerge root folder. Because all
463 other code assumes we are in WinMerge root folder. If the root folder is
464 found current folder is changed into it."""
466 # If we find Src and Filters -subfolders we are in root
467 if os.path.exists('Src') and os.path.exists('Filters'):
470 # Check if we are in /Tools/Scripts
471 if os.path.exists('../../Src') and os.path.exists('../../Filters'):
478 """Check that needed external tools can be found."""
481 if not os.path.exists(prog.svn_binary):
482 print 'Subversion binary could not be found from:'
483 print prog.svn_binary
484 print 'Please check script configuration and/or make sure Subversion is installed.'
487 vs_cmd = get_vs_ide_bin()
488 if not os.path.exists(vs_cmd):
489 print 'Cannot find Visual Studio IDE binary from:'
491 print 'Please check script configuration.'
494 pathhhc = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/hhc/hhc.exe')
495 folderdtd = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/dtd')
496 foldersaxon = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/saxon')
497 folderxerc = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/xerces')
498 folderxsl = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/xsl')
500 if not os.path.exists(pathhhc) or not os.path.exists(folderdtd) or \
501 not os.path.exists(foldersaxon) or not os.path.exists(folderxerc) or \
502 not os.path.exists(folderxsl):
503 print 'Cannot find manual build tools'
504 print 'Please download and install manual build tools from:'
505 print 'https://sourceforge.net/project/showfiles.php?group_id=13216'
506 print 'See also Docs/Developers/readme-manual.html'
510 def check_x64shellext():
511 """Checks that 64-bit ShellExtension is compiled prior to running this
514 This is due to the fact we can't compile 64-bit ShellExtension without some
515 environment tweaks, so it won't work (currently) from this script. And the
516 ShellExtension must be compiled separately.
518 if not os.path.exists('build/shellextensionx64/ShellExtensionX64.dll'):
519 print 'ERROR: cannot create a release:'
520 print 'You must compile 64-bit ShellExtension (ShellExtensionX64.dll)'
521 print 'before running this script!'
527 """Print script usage information."""
529 print 'WinMerge release script.'
530 print 'Usage: create_release [-h] [-f file] [-v n] [-c] [-l]'
532 print ' -h, --help print this help'
533 print ' -v n, --version=n set release version'
534 print ' -c, --cleanup clean up build files (temp files, libraries, executables)'
535 print ' -l, --libraries build libraries (expat, scew, pcre) only'
536 print ' -f file, --file=filename set the version number ini file'
537 print ' For example: create_release -f versions.ini'
538 print ' If no version number (-v) or INI file (-f) given, 0.0.0.0 will be'
539 print ' used as version number.'
545 opts, args = getopt.getopt(argv, "hclv:f:", [ "help", "cleanup", "libraries",
546 "version=", "file="])
548 for opt, arg in opts:
549 if opt in ("-h", "--help"):
552 if opt in ("-v", "--version"):
554 print "Start building WinMerge release version " + prog.version
555 if opt in ("-c", "--cleanup"):
556 if cleanup_build() == True:
557 print 'Cleanup done.'
559 if opt in ("-l", "--libraries"):
562 if opt in ("-f", "--file"):
565 if ver_file == '' and prog.version == '':
566 print 'WARNING: No version number or INI file given, using default'
567 print ' version number of 0.0.0.0 where applicable in this script.'
568 prog.version = '0.0.0.0'
570 # Check we are running from correct folder (and go to root if found)
571 if find_winmerge_root() == False:
572 print 'ERROR: Cannot find WinMerge root folder!'
573 print 'The script must be run from WinMerge tree\'s root folder'
574 print '(which has Src- and Filter -folders as subfolders) or from'
575 print 'Tools/Scripts -folder (where this script is located).'
578 # Now read settings from Tools.ini
579 prog.read_ini('Tools.ini')
580 print 'Compiler: ' + prog.vs_path
581 print 'Path:' + os.getcwd()
583 # Remember the rootfolder
584 prog.rootpath = os.getcwd()
586 # Check all required tools are found (script configuration)
587 if check_tools() == False:
590 # Check 64-bit ShellExtension is compiled
591 if check_x64shellext() == False:
594 # Create the distribution folder if it doesn't exist
596 if not os.path.exists(dist_root_folder):
597 os.mkdir(dist_root_folder)
598 except EnvironmentError, einst:
599 print 'Error creating distribution folder: ' + dist_root_folder
603 # Remove old build's files
604 if cleanup_build() == False:
607 if len(ver_file) > 0:
608 version_read = get_product_version(ver_file)
609 if len(version_read) > 0:
610 prog.version = version_read
611 set_resource_version(ver_file)
613 version_folder = 'WinMerge-' + prog.version
614 dist_folder = get_and_create_dist_folder(version_folder)
615 if dist_folder == '':
617 dist_src_folder = get_src_dist_folder(dist_folder, version_folder)
618 svn_export(dist_src_folder)
624 build_innosetup_installer(dist_folder)
626 dist_bin_folder = get_and_create_bin_folder(dist_folder, version_folder)
627 create_bin_folders(dist_bin_folder, dist_src_folder)
629 # Do the cleanup after creating binary distrib folders, as some files
630 # and folders are copied from source folders to binary folders.
631 cleanup_dlls_from_plugins(dist_src_folder)
633 print 'WinMerge release script ready!'
637 if __name__ == "__main__":