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 retval = call(['cscript', '/nologo', 'CreateMasterPotFile.vbs'])
259 retval = call(['cscript', '/nologo', 'UpdatePoFilesFromPotFile.vbs'])
265 print 'ERROR: Updating translations failed!'
268 def get_and_create_dist_folder(folder):
269 """Formats a folder name for version-specific distribution folder
270 and creates the folder."""
272 abs_folder = os.path.realpath(dist_root_folder)
273 dist_folder = os.path.join(abs_folder, folder)
274 if os.path.exists(dist_folder):
275 print 'Folder: ' + dist_folder + ' already exists!'
276 print 'If you want to re-create this version, remove folder first!'
279 print 'Create distribution folder: ' + dist_folder
280 os.mkdir(dist_folder)
283 def get_src_dist_folder(dist_folder, folder):
284 """Format a source distribution folder path."""
286 dist_src = os.path.join(dist_folder, folder + '-src')
289 def svn_export(dist_src_folder):
290 """Exports sources to distribution folder."""
292 print 'Exporting sources to ' + dist_src_folder
293 print 'Exporting from: ' + prog.source
295 if prog.source == 'workspace':
296 retval = call([prog.svn_binary, 'export', '--non-interactive', '.', dist_src_folder])
298 retval = call([prog.svn_binary, 'export', '--non-interactive', source_location, dist_src_folder])
302 print 'Error exporting sources! SVN return value: ' + retval
305 def cleanup_dlls_from_plugins(dist_src_folder):
306 """Remove compiled plugin dll files from source distribution folders."""
308 dll_folder = os.path.join(dist_src_folder, 'Plugins/dlls')
309 files = os.listdir(dll_folder)
311 print 'Removing dll files from plugin folder...'
312 for cur_file in files:
313 fullpath = os.path.join(dll_folder, cur_file)
314 if os.path.isfile(fullpath):
315 file_name, file_ext = os.path.splitext(cur_file)
316 if (file_ext == '.dll'):
319 def build_libraries():
320 """Builds library targets: expat, scew and pcre."""
322 vs_cmd = get_vs_ide_bin()
323 cur_path = os.getcwd()
325 print 'Build expat library...'
326 solution_path = os.path.join(cur_path, 'Externals/expat/lib/expat.vcproj')
328 call([vs_cmd, solution_path, '/rebuild', 'Release'], shell=True)
330 print 'Build scew library...'
331 solution_path = os.path.join(cur_path, 'Externals/scew/win32/scew.vcproj')
333 call([vs_cmd, solution_path, '/rebuild', 'Release'], shell=True)
335 print 'Build pcre library...'
336 solution_path = os.path.join(cur_path, 'Externals/pcre/Win32/pcre.vcproj')
338 call([vs_cmd, solution_path, '/rebuild', 'MinSizeRel'], shell=True)
341 """Builds all WinMerge targets."""
345 vs_cmd = get_vs_ide_bin()
347 ret = build_winmerge(vs_cmd)
349 ret = build_shellext(vs_cmd)
352 def build_winmerge(vs_cmd):
353 """Builds WinMerge executable targets."""
355 cur_path = os.getcwd()
356 solution_path = os.path.join(cur_path, 'Src\\Merge.vcproj')
359 # devenv Src\Merge.dsp /rebuild Release
360 print 'Build WinMerge executables...'
361 ret = call([vs_cmd, solution_path, '/rebuild', 'Release'], shell = True)
363 ret = call([vs_cmd, solution_path, '/rebuild', 'UnicodeRelease'], shell = True)
365 print 'ERROR: Failed to build ANSI release target of WinMerge!'
371 print 'ERROR: Failed to build Unicode release target of WinMerge!'
374 def build_shellext(vs_cmd):
375 """Builds 32-bit ShellExtension."""
377 cur_path = os.getcwd()
378 solution_path = os.path.join(cur_path, 'ShellExtension\\ShellExtension.vcproj')
380 # devenv Src\Merge.dsp /rebuild Release
381 print 'Build ShellExtension dlls...'
382 ret = call([vs_cmd, solution_path, '/rebuild', 'Release MinDependency'])
384 ret = call([vs_cmd, solution_path, '/rebuild', 'Unicode Release MinDependency'])
386 print 'ERROR: Failed to build ANSI target of ShellExtension!'
392 print 'ERROR: Failed to build Unicode target of ShellExtension!'
396 """Builds manual's HTML Help (CHM) version for user install and
397 HTML version for the Web. HTML version is created with ads."""
400 os.chdir('Docs/Users/Manual/build')
401 print 'Build HTML Help (CHM) manual...'
402 call(['build_htmlhelp.bat'])
404 # HTML manual not build in trunk.
405 #print 'Build HTML manual for Web with ads...'
406 #call(['build_html.bat', 'withads'])
407 print 'Manual build finished.'
410 def build_innosetup_installer(target_folder):
411 """Builds the InnoSetup installer for the WinMerge."""
413 innosetup_exe = os.path.join(prog.innosetup_path, 'iscc.exe')
414 cur_path = os.getcwd()
416 prog.winmerge_iss_path = os.path.join(cur_path, 'Installer\\InnoSetup\\' + prog.winmerge_iss)
418 #output_switch = '/O"' + target_folder + '"'
420 print 'Build Innosetup installer...'
421 # Should be able to give folder for created file and Q switch to make build quiet
422 #call([innosetup_exe, '/Q', output_switch, winmerge_iss])
423 call([innosetup_exe, prog.winmerge_iss_path])
425 def get_and_create_bin_folder(dist_folder, folder):
426 """Formats and creates binary distribution folder."""
428 bin_folder = os.path.join(dist_folder, folder + '-exe')
429 print 'Create binary distribution folder: ' + bin_folder
433 def create_bin_folders(bin_folder, dist_src_folder):
434 """Creates binary distribution folders."""
436 cur_path = os.getcwd()
438 print 'Create binary distribution folder structure...'
439 lang_folder = os.path.join(bin_folder, 'Languages')
440 os.mkdir(lang_folder)
441 doc_folder = os.path.join(bin_folder, 'Docs')
443 filters_folder = os.path.join(bin_folder, 'Filters')
444 plugins_folder = os.path.join(bin_folder, 'MergePlugins')
447 print 'Copying files to binary distribution folder...'
448 shutil.copy('build/mergerelease/WinMerge.exe', bin_folder)
449 shutil.copy('build/mergeunicoderelease/WinMergeU.exe', bin_folder)
451 shutil.copy('build/mergerelease/ShellExtension.dll', bin_folder)
452 shutil.copy('build/mergeunicoderelease/ShellExtensionU.dll', bin_folder)
453 shutil.copy('build/mergeunicoderelease/MergeLang.dll', bin_folder)
454 shutil.copy('build/shellextensionx64/ShellExtensionX64.dll', bin_folder)
455 shutil.copy('ShellExtension/Register.bat', bin_folder)
456 shutil.copy('ShellExtension/UnRegister.bat', bin_folder)
458 shutil.copy('build/pcre/pcre.dll', bin_folder)
459 shutil.copy('build/expat/libexpat.dll', bin_folder)
461 copy_po_files(lang_folder)
462 filter_orig = os.path.join(dist_src_folder, 'Filters')
463 shutil.copytree(filter_orig, filters_folder, False)
465 # Copy compiled plugins dir and rename it
466 plugin_dir = os.path.join(bin_folder, 'dlls')
467 plugin_orig = os.path.join(dist_src_folder, 'Plugins/dlls')
468 shutil.copytree(plugin_orig, plugin_dir, False)
469 os.rename(plugin_dir, plugins_folder)
471 shutil.copy('build/Manual/htmlhelp/WinMerge.chm', doc_folder)
473 shutil.copy('Docs/Users/ReleaseNotes.html', doc_folder)
474 shutil.copy('Docs/Users/ReadMe.txt', bin_folder)
475 shutil.copy('Docs/Users/ChangeLog.txt', doc_folder)
476 shutil.copy('Docs/Users/Contributors.txt', bin_folder)
477 shutil.copy('Docs/Users/Files.txt', bin_folder)
479 def copy_po_files(dest_folder):
480 """Copies all PO files to destination folder."""
482 lang_folder = 'Translations/WinMerge'
483 files = os.listdir(lang_folder)
485 print 'Copying PO files to binary folder...'
486 for cur_file in files:
487 fullpath = os.path.join(lang_folder, cur_file)
488 if os.path.isfile(fullpath):
489 file_name, file_ext = os.path.splitext(cur_file)
490 if (file_ext == '.po'):
491 shutil.copy(fullpath, dest_folder)
493 def find_winmerge_root():
494 """Find WinMerge tree root folder from where to run rest of the script.
496 This function checks if we are in WinMerge root folder. If we are in some
497 other folder then we must try to find the WinMerge root folder. Because all
498 other code assumes we are in WinMerge root folder. If the root folder is
499 found current folder is changed into it."""
501 # If we find Src and Filters -subfolders we are in root
502 if os.path.exists('Src') and os.path.exists('Filters'):
505 # Check if we are in /Tools/Scripts
506 if os.path.exists('../../Src') and os.path.exists('../../Filters'):
513 """Check that needed external tools can be found."""
516 if not os.path.exists(prog.svn_binary):
517 print 'Subversion binary could not be found from:'
518 print prog.svn_binary
519 print 'Please check script configuration and/or make sure Subversion is installed.'
522 vs_cmd = get_vs_ide_bin()
523 if not os.path.exists(vs_cmd):
524 print 'Cannot find Visual Studio IDE binary from:'
526 print 'Please check script configuration.'
529 pathhhc = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/hhc/hhc.exe')
530 folderdtd = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/dtd')
531 foldersaxon = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/saxon')
532 folderxerc = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/xerces')
533 folderxsl = os.path.join(prog.rootpath, 'Docs/Users/Manual/build/xsl')
535 if not os.path.exists(pathhhc) or not os.path.exists(folderdtd) or \
536 not os.path.exists(foldersaxon) or not os.path.exists(folderxerc) or \
537 not os.path.exists(folderxsl):
538 print 'Cannot find manual build tools'
539 print 'Please download and install manual build tools from:'
540 print 'https://sourceforge.net/project/showfiles.php?group_id=13216'
541 print 'See also Docs/Developers/readme-manual.html'
545 def check_x64shellext():
546 """Checks that 64-bit ShellExtension is compiled prior to running this
549 This is due to the fact we can't compile 64-bit ShellExtension without some
550 environment tweaks, so it won't work (currently) from this script. And the
551 ShellExtension must be compiled separately.
553 if not os.path.exists('build/shellextensionx64/ShellExtensionX64.dll'):
554 print 'ERROR: cannot create a release:'
555 print 'You must compile 64-bit ShellExtension (ShellExtensionX64.dll)'
556 print 'before running this script!'
562 """Print script usage information."""
564 print 'WinMerge release script.'
565 print 'Usage: create_release [-h] [-f file] [-v n] [-c] [-l]'
567 print ' -h, --help print this help'
568 print ' -v n, --version=n set release version'
569 print ' -c, --cleanup clean up build files (temp files, libraries, executables)'
570 print ' -l, --libraries build libraries (expat, scew, pcre) only'
571 print ' -f file, --file=filename set the version number ini file'
572 print ' For example: create_release -f versions.ini'
573 print ' If no version number (-v) or INI file (-f) given, 0.0.0.0 will be'
574 print ' used as version number.'
580 opts, args = getopt.getopt(argv, "hclv:f:", [ "help", "cleanup", "libraries",
581 "version=", "file="])
583 for opt, arg in opts:
584 if opt in ("-h", "--help"):
587 if opt in ("-v", "--version"):
589 print "Start building WinMerge release version " + prog.version
590 if opt in ("-c", "--cleanup"):
591 if cleanup_build() == True:
592 print 'Cleanup done.'
594 if opt in ("-l", "--libraries"):
597 if opt in ("-f", "--file"):
600 if ver_file == '' and prog.version == '':
601 print 'WARNING: No version number or INI file given, using default'
602 print ' version number of 0.0.0.0 where applicable in this script.'
603 prog.version = '0.0.0.0'
605 # Check we are running from correct folder (and go to root if found)
606 if find_winmerge_root() == False:
607 print 'ERROR: Cannot find WinMerge root folder!'
608 print 'The script must be run from WinMerge tree\'s root folder'
609 print '(which has Src- and Filter -folders as subfolders) or from'
610 print 'Tools/Scripts -folder (where this script is located).'
613 # Now read settings from Tools.ini
614 prog.read_ini('Tools.ini')
615 print 'Compiler: ' + prog.vs_path
616 print 'Path:' + os.getcwd()
618 # Remember the rootfolder
619 prog.rootpath = os.getcwd()
621 # Check all required tools are found (script configuration)
622 if check_tools() == False:
625 # Check 64-bit ShellExtension is compiled
626 if check_x64shellext() == False:
629 # Create the distribution folder if it doesn't exist
631 if not os.path.exists(dist_root_folder):
632 os.mkdir(dist_root_folder)
633 except EnvironmentError, einst:
634 print 'Error creating distribution folder: ' + dist_root_folder
638 # Remove old build's files
639 if cleanup_build() == False:
642 if len(ver_file) > 0:
643 version_read = get_product_version(ver_file)
644 if len(version_read) > 0:
645 prog.version = version_read
646 set_resource_version(ver_file)
648 version_folder = 'WinMerge-' + prog.version
649 dist_folder = get_and_create_dist_folder(version_folder)
650 if dist_folder == '':
652 dist_src_folder = get_src_dist_folder(dist_folder, version_folder)
653 if svn_export(dist_src_folder) == False:
656 if setup_translations() == False:
659 if build_targets() == False:
662 build_innosetup_installer(dist_folder)
664 dist_bin_folder = get_and_create_bin_folder(dist_folder, version_folder)
665 create_bin_folders(dist_bin_folder, dist_src_folder)
667 # Do the cleanup after creating binary distrib folders, as some files
668 # and folders are copied from source folders to binary folders.
669 cleanup_dlls_from_plugins(dist_src_folder)
671 print 'WinMerge release script ready!'
675 if __name__ == "__main__":