2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Script to create Chrome Installer archive.
8 This script is used to create an archive of all the files required for a
9 Chrome install in appropriate directory structure. It reads chrome.release
10 file as input, creates chrome.7z archive, compresses setup.exe and
11 generates packed_files.txt for mini_installer project.
24 ARCHIVE_DIR
= "installer_archive"
26 # suffix to uncompresed full archive file, appended to options.output_name
27 ARCHIVE_SUFFIX
= ".7z"
28 BSDIFF_EXEC
= "bsdiff.exe"
29 CHROME_DIR
= "Chrome-bin"
30 CHROME_PATCH_FILE_SUFFIX
= "_patch" # prefixed by options.output_name
32 # compressed full archive suffix, will be prefixed by options.output_name
33 COMPRESSED_ARCHIVE_SUFFIX
= ".packed.7z"
35 COMPRESSED_FILE_EXT
= ".packed.7z" # extension of patch archive file
36 COURGETTE_EXEC
= "courgette.exe"
37 MINI_INSTALLER_INPUT_FILE
= "packed_files.txt"
38 PATCH_FILE_EXT
= '.diff'
39 SETUP_EXEC
= "setup.exe"
40 SETUP_PATCH_FILE_PREFIX
= "setup_patch"
41 TEMP_ARCHIVE_DIR
= "temp_installer_archive"
42 VERSION_FILE
= "VERSION"
45 def BuildVersion(build_dir
):
46 """Returns the full build version string constructed from information in
47 VERSION_FILE. Any segment not found in that file will default to '0'.
53 for line
in open(os
.path
.join(build_dir
, '../../chrome', VERSION_FILE
), 'r'):
55 if line
.startswith('MAJOR='):
57 elif line
.startswith('MINOR='):
59 elif line
.startswith('BUILD='):
61 elif line
.startswith('PATCH='):
63 return '%s.%s.%s.%s' % (major
, minor
, build
, patch
)
66 def CompressUsingLZMA(build_dir
, compressed_file
, input_file
):
67 lzma_exec
= GetLZMAExec(build_dir
)
70 # Flags equivalent to -mx9 (ultra) but with the bcj2 turned on (exe
71 # pre-filter). This results in a ~2.3MB decrease in installer size on
73 # Additionally, these settings reflect a 7zip 4.42 and up change in
74 # the definition of -mx9, increasting the dicionary size moving to
75 # 26bit = 64MB. This results in an additional ~3.5MB decrease.
76 # Older 7zip versions can support these settings, as these changes
77 # rely on existing functionality in the lzma format.
80 '-m2=LZMA:d22:fb128:mf=bt2',
81 '-m3=LZMA:d22:fb128:mf=bt2',
87 if os
.path
.exists(compressed_file
):
88 os
.remove(compressed_file
)
92 def CopyAllFilesToStagingDir(config
, distribution
, staging_dir
, build_dir
,
93 enable_hidpi
, enable_touch_ui
):
94 """Copies the files required for installer archive.
95 Copies all common files required for various distributions of Chromium and
96 also files for the specific Chromium build specified by distribution.
98 CopySectionFilesToStagingDir(config
, 'GENERAL', staging_dir
, build_dir
)
100 if len(distribution
) > 1 and distribution
[0] == '_':
101 distribution
= distribution
[1:]
102 CopySectionFilesToStagingDir(config
, distribution
.upper(),
103 staging_dir
, build_dir
)
104 if enable_hidpi
== '1':
105 CopySectionFilesToStagingDir(config
, 'HIDPI', staging_dir
, build_dir
)
106 if enable_touch_ui
== '1':
107 CopySectionFilesToStagingDir(config
, 'TOUCH', staging_dir
, build_dir
)
110 def CopySectionFilesToStagingDir(config
, section
, staging_dir
, src_dir
):
111 """Copies installer archive files specified in section from src_dir to
112 staging_dir. This method reads section from config and copies all the
113 files specified from src_dir to staging dir.
115 for option
in config
.options(section
):
116 if option
.endswith('dir'):
119 dst_dir
= os
.path
.join(staging_dir
, config
.get(section
, option
))
120 src_paths
= glob
.glob(os
.path
.join(src_dir
, option
))
121 if src_paths
and not os
.path
.exists(dst_dir
):
123 for src_path
in src_paths
:
124 dst_path
= os
.path
.join(dst_dir
, os
.path
.basename(src_path
))
125 if not os
.path
.exists(dst_path
):
126 shutil
.copy(src_path
, dst_dir
)
128 def GenerateDiffPatch(options
, orig_file
, new_file
, patch_file
):
129 if (options
.diff_algorithm
== "COURGETTE"):
130 exe_file
= os
.path
.join(options
.last_chrome_installer
, COURGETTE_EXEC
)
131 cmd
= '%s -gen "%s" "%s" "%s"' % (exe_file
, orig_file
, new_file
, patch_file
)
133 exe_file
= os
.path
.join(options
.build_dir
, BSDIFF_EXEC
)
134 cmd
= [exe_file
, orig_file
, new_file
, patch_file
,]
135 RunSystemCommand(cmd
)
137 def GetLZMAExec(build_dir
):
138 lzma_exec
= os
.path
.join(build_dir
, "..", "..", "third_party",
139 "lzma_sdk", "Executable", "7za.exe")
142 def GetPrevVersion(build_dir
, temp_dir
, last_chrome_installer
, output_name
):
143 if not last_chrome_installer
:
146 lzma_exec
= GetLZMAExec(build_dir
)
147 prev_archive_file
= os
.path
.join(last_chrome_installer
,
148 output_name
+ ARCHIVE_SUFFIX
)
153 'Chrome-bin/*/chrome.dll',]
154 RunSystemCommand(cmd
)
155 dll_path
= glob
.glob(os
.path
.join(temp_dir
, 'Chrome-bin', '*', 'chrome.dll'))
156 return os
.path
.split(os
.path
.split(dll_path
[0])[0])[1]
158 def MakeStagingDirectories(staging_dir
):
159 """Creates a staging path for installer archive. If directory exists already,
160 deletes the existing directory.
162 file_path
= os
.path
.join(staging_dir
, TEMP_ARCHIVE_DIR
)
163 if os
.path
.exists(file_path
):
164 shutil
.rmtree(file_path
)
165 os
.makedirs(file_path
)
167 temp_file_path
= os
.path
.join(staging_dir
, TEMP_ARCHIVE_DIR
)
168 if os
.path
.exists(temp_file_path
):
169 shutil
.rmtree(temp_file_path
)
170 os
.makedirs(temp_file_path
)
171 return (file_path
, temp_file_path
)
173 def Readconfig(input_file
, current_version
):
174 """Reads config information from input file after setting default value of
178 variables
['ChromeDir'] = CHROME_DIR
179 variables
['VersionDir'] = os
.path
.join(variables
['ChromeDir'],
181 config
= ConfigParser
.SafeConfigParser(variables
)
182 config
.read(input_file
)
185 def RunSystemCommand(cmd
, **kw
):
187 exit_code
= subprocess
.call(cmd
, **kw
)
189 raise Exception("Error while running cmd: %s, exit_code: %s" %
192 def CreateArchiveFile(options
, staging_dir
, current_version
, prev_version
):
193 """Creates a new installer archive file after deleting any existing old file.
195 # First create an uncompressed archive file for the current build (chrome.7z)
196 lzma_exec
= GetLZMAExec(options
.build_dir
)
197 archive_file
= os
.path
.join(options
.output_dir
,
198 options
.output_name
+ ARCHIVE_SUFFIX
)
203 os
.path
.join(staging_dir
, CHROME_DIR
),
205 # There doesnt seem to be any way in 7za.exe to override existing file so
206 # we always delete before creating a new one.
207 if not os
.path
.exists(archive_file
):
208 RunSystemCommand(cmd
)
209 elif options
.skip_rebuild_archive
!= "true":
210 os
.remove(archive_file
)
211 RunSystemCommand(cmd
)
213 # Do not compress the archive in developer (component) builds.
214 if options
.component_build
== '1':
215 compressed_file
= os
.path
.join(
216 options
.output_dir
, options
.output_name
+ COMPRESSED_ARCHIVE_SUFFIX
)
217 if os
.path
.exists(compressed_file
):
218 os
.remove(compressed_file
)
219 return os
.path
.basename(archive_file
)
221 # If we are generating a patch, run bsdiff against previous build and
222 # compress the resulting patch file. If this is not a patch just compress the
223 # uncompressed archive file.
224 patch_name_prefix
= options
.output_name
+ CHROME_PATCH_FILE_SUFFIX
225 if options
.last_chrome_installer
:
226 prev_archive_file
= os
.path
.join(options
.last_chrome_installer
,
227 options
.output_name
+ ARCHIVE_SUFFIX
)
228 patch_file
= os
.path
.join(options
.build_dir
, patch_name_prefix
+
230 GenerateDiffPatch(options
, prev_archive_file
, archive_file
, patch_file
)
231 compressed_archive_file
= patch_name_prefix
+ '_' + \
232 current_version
+ '_from_' + prev_version
+ \
234 orig_file
= patch_file
236 compressed_archive_file
= options
.output_name
+ COMPRESSED_ARCHIVE_SUFFIX
237 orig_file
= archive_file
239 compressed_archive_file_path
= os
.path
.join(options
.output_dir
,
240 compressed_archive_file
)
241 CompressUsingLZMA(options
.build_dir
, compressed_archive_file_path
, orig_file
)
243 return compressed_archive_file
246 def PrepareSetupExec(options
, current_version
, prev_version
):
247 """Prepares setup.exe for bundling in mini_installer based on options."""
248 if options
.setup_exe_format
== "FULL":
249 setup_file
= SETUP_EXEC
250 elif options
.setup_exe_format
== "DIFF":
251 if not options
.last_chrome_installer
:
253 "To use DIFF for setup.exe, --last_chrome_installer is needed.")
254 prev_setup_file
= os
.path
.join(options
.last_chrome_installer
, SETUP_EXEC
)
255 new_setup_file
= os
.path
.join(options
.build_dir
, SETUP_EXEC
)
256 patch_file
= os
.path
.join(options
.build_dir
, SETUP_PATCH_FILE_PREFIX
+
258 GenerateDiffPatch(options
, prev_setup_file
, new_setup_file
, patch_file
)
259 setup_file
= SETUP_PATCH_FILE_PREFIX
+ '_' + current_version
+ \
260 '_from_' + prev_version
+ COMPRESSED_FILE_EXT
261 setup_file_path
= os
.path
.join(options
.build_dir
, setup_file
)
262 CompressUsingLZMA(options
.build_dir
, setup_file_path
, patch_file
)
264 cmd
= ['makecab.exe',
265 '/D', 'CompressionType=LZX',
267 '/L', options
.output_dir
,
268 os
.path
.join(options
.build_dir
, SETUP_EXEC
),]
269 # Send useless makecab progress on stdout to the bitbucket.
270 RunSystemCommand(cmd
, stdout
=open(os
.devnull
, "w"))
271 setup_file
= SETUP_EXEC
[:-1] + "_"
275 _RESOURCE_FILE_TEMPLATE
= """\
276 // This file is automatically generated by create_installer_archive.py.
277 // It contains the resource entries that are going to be linked inside
278 // mini_installer.exe. For each file to be linked there should be two
280 // - The first line contains the output filename (without path) and the
281 // type of the resource ('BN' - not compressed , 'BL' - LZ compressed,
282 // 'B7' - LZMA compressed)
283 // - The second line contains the path to the input file. Uses '/' to
284 // separate path components.
286 %(setup_file)s %(setup_file_resource_type)s
287 "%(setup_file_path)s"
290 "%(archive_file_path)s"
294 def CreateResourceInputFile(
295 output_dir
, setup_format
, archive_file
, setup_file
, resource_file_path
):
296 """Creates resource input file (packed_files.txt) for mini_installer project.
298 This method checks the format of setup.exe being used and according sets
301 setup_resource_type
= "BL"
302 if (setup_format
== "FULL"):
303 setup_resource_type
= "BN"
304 elif (setup_format
== "DIFF"):
305 setup_resource_type
= "B7"
307 # Expand the resource file template.
309 'setup_file': setup_file
,
310 'setup_file_resource_type': setup_resource_type
,
312 os
.path
.join(output_dir
, setup_file
).replace("\\","/"),
313 'archive_file': archive_file
,
315 os
.path
.join(output_dir
, archive_file
).replace("\\","/"),
317 resource_file
= _RESOURCE_FILE_TEMPLATE
% args
319 with
open(resource_file_path
, 'w') as f
:
320 f
.write(resource_file
)
323 # Reads |manifest_name| from |build_dir| and writes |manifest_name| to
324 # |output_dir| with the same content plus |inserted_string| added just before
326 def CopyAndAugmentManifest(build_dir
, output_dir
, manifest_name
,
327 inserted_string
, insert_before
):
328 with
open(os
.path
.join(build_dir
, manifest_name
), 'r') as f
:
329 manifest_lines
= f
.readlines()
333 for i
in xrange(len(manifest_lines
)):
334 insert_pos
= manifest_lines
[i
].find(insert_before
)
338 if insert_line
== -1:
339 raise ValueError('Could not find {0} in the manifest:\n{1}'.format(
340 insert_before
, ''.join(manifest_lines
)))
341 old
= manifest_lines
[insert_line
]
342 manifest_lines
[insert_line
] = (old
[:insert_pos
] + '\n' + inserted_string
+
343 '\n' + old
[insert_pos
:])
345 with
open(os
.path
.join(output_dir
, manifest_name
), 'w') as f
:
346 f
.write(''.join(manifest_lines
))
349 def CopyIfChanged(src
, target_dir
):
350 """Copy specified |src| file to |target_dir|, but only write to target if
351 the file has changed. This avoids a problem during packaging where parts of
352 the build have not completed and have the runtime DLL locked when we try to
353 copy over it. See http://crbug.com/305877 for details."""
354 assert os
.path
.isdir(target_dir
)
355 dest
= os
.path
.join(target_dir
, os
.path
.basename(src
))
356 if os
.path
.exists(dest
):
357 # We assume the files are OK to buffer fully into memory since we know
359 with
open(src
, 'rb') as fsrc
:
360 src_data
= fsrc
.read()
361 with
open(dest
, 'rb') as fdest
:
362 dest_data
= fdest
.read()
363 if src_data
!= dest_data
:
364 # This may still raise if we get here, but this really should almost
365 # never happen (it would mean that the contents of e.g. msvcr100d.dll
367 shutil
.copyfile(src
, dest
)
369 shutil
.copyfile(src
, dest
)
372 # Copy the relevant CRT DLLs to |build_dir|. We copy DLLs from all versions
373 # of VS installed to make sure we have the correct CRT version, unused DLLs
374 # should not conflict with the others anyways.
375 def CopyVisualStudioRuntimeDLLs(build_dir
, target_arch
):
376 is_debug
= os
.path
.basename(build_dir
).startswith('Debug')
377 if not is_debug
and not os
.path
.basename(build_dir
).startswith('Release'):
378 print ("Warning: could not determine build configuration from "
379 "output directory, assuming Release build.")
384 crt_dlls
= glob
.glob(
385 "C:/Program Files (x86)/Microsoft Visual Studio */VC/redist/"
386 "Debug_NonRedist/" + target_arch
+ "/Microsoft.*.DebugCRT/*.dll")
388 crt_dlls
= glob
.glob(
389 "C:/Program Files (x86)/Microsoft Visual Studio */VC/redist/" +
390 target_arch
+ "/Microsoft.*.CRT/*.dll")
392 # Also handle the case where someone is building using only winsdk and
393 # doesn't have Visual Studio installed.
395 if target_arch
== 'x64':
396 # check we are are on a 64bit system by existence of WOW64 dir
397 if os
.access("C:/Windows/SysWOW64", os
.F_OK
):
398 sys_dll_dir
= "C:/Windows/System32"
400 # only support packaging of 64bit installer on 64bit system
401 # but this just as bad as not finding DLLs at all so we
402 # don't abort here to mirror behavior below
403 print ("Warning: could not find x64 CRT DLLs on x86 system.")
405 # On a 64-bit system, 32-bit dlls are in SysWOW64 (don't ask).
406 if os
.access("C:/Windows/SysWOW64", os
.F_OK
):
407 sys_dll_dir
= "C:/Windows/SysWOW64"
409 sys_dll_dir
= "C:/Windows/System32"
411 if sys_dll_dir
is not None:
413 crt_dlls
= glob
.glob(os
.path
.join(sys_dll_dir
, "msvc*0d.dll"))
415 crt_dlls
= glob
.glob(os
.path
.join(sys_dll_dir
, "msvc*0.dll"))
418 print ("Warning: could not find CRT DLLs to copy to build dir - target "
419 "may not run on a system that doesn't have those DLLs.")
422 CopyIfChanged(dll
, build_dir
)
425 # Copies component build DLLs and generates required config files and manifests
426 # in order for chrome.exe and setup.exe to be able to find those DLLs at
428 # This is meant for developer builds only and should never be used to package
430 def DoComponentBuildTasks(staging_dir
, build_dir
, target_arch
, current_version
):
431 # Get the required directories for the upcoming operations.
432 chrome_dir
= os
.path
.join(staging_dir
, CHROME_DIR
)
433 version_dir
= os
.path
.join(chrome_dir
, current_version
)
434 installer_dir
= os
.path
.join(version_dir
, 'Installer')
435 # |installer_dir| is technically only created post-install, but we need it
436 # now to add setup.exe's config and manifest to the archive.
437 if not os
.path
.exists(installer_dir
):
438 os
.mkdir(installer_dir
)
440 # Copy the VS CRT DLLs to |build_dir|. This must be done before the general
441 # copy step below to ensure the CRT DLLs are added to the archive and marked
442 # as a dependency in the exe manifests generated below.
443 CopyVisualStudioRuntimeDLLs(build_dir
, target_arch
)
445 # Stage all the component DLLs found in |build_dir|. These are all the DLLs
446 # which have not already been added to the staged |version_dir| by virtue of
448 build_dlls
= glob
.glob(os
.path
.join(build_dir
, '*.dll'))
449 staged_dll_basenames
= [os
.path
.basename(staged_dll
) for staged_dll
in \
450 glob
.glob(os
.path
.join(version_dir
, '*.dll'))]
451 component_dll_filenames
= []
452 for component_dll
in [dll
for dll
in build_dlls
if \
453 os
.path
.basename(dll
) not in staged_dll_basenames
]:
454 # remoting_*.dll's don't belong in the archive (it doesn't depend on them
455 # in gyp). Trying to copy them causes a build race when creating the
456 # installer archive in component mode. See: crbug.com/180996
457 if os
.path
.basename(component_dll
).startswith('remoting_'):
459 # Copy them to the version_dir (for the version assembly to be able to refer
460 # to them below and make sure chrome.exe can find them at runtime).
461 shutil
.copy(component_dll
, version_dir
)
462 # Also copy them directly to the Installer directory for the installed
463 # setup.exe to be able to run (as it doesn't statically link in component
465 # This makes the archive ~1.5X bigger (Release ~185MB => ~278MB;
466 # Debug ~520MB => ~875MB) this is however simpler than any other installer
467 # change and doesn't make archive generation itself slower so it only
468 # matters when copying the archive to other test machines. This approach
469 # can be revised if this is a problem.
470 shutil
.copy(component_dll
, installer_dir
)
471 component_dll_filenames
.append(os
.path
.basename(component_dll
))
473 # Copy chrome.exe.manifest as-is. It is required, among other things, to
474 # declare a dependency on the version dir assembly.
475 shutil
.copy(os
.path
.join(build_dir
, 'chrome.exe.manifest'), chrome_dir
)
477 # Also copy setup.exe.manifest as-is. It is required, among other things, to
478 # let setup.exe UAC when it wants to, by specifying that it handles elevation
479 # "asInvoker", rather than have Windows auto-elevate it when launched.
480 shutil
.copy(os
.path
.join(build_dir
, 'setup.exe.manifest'), installer_dir
)
482 # Augment {version}.manifest to include all component DLLs as part of the
483 # assembly it constitutes, which will allow dependents of this assembly to
485 version_assembly_dll_additions
= []
486 for dll_filename
in component_dll_filenames
:
487 version_assembly_dll_additions
.append(" <file name='%s'/>" % dll_filename
)
488 CopyAndAugmentManifest(build_dir
, version_dir
,
489 '%s.manifest' % current_version
,
490 '\n'.join(version_assembly_dll_additions
),
495 """Main method that reads input file, creates archive file and write
498 current_version
= BuildVersion(options
.build_dir
)
500 config
= Readconfig(options
.input_file
, current_version
)
502 (staging_dir
, temp_dir
) = MakeStagingDirectories(options
.staging_dir
)
504 prev_version
= GetPrevVersion(options
.build_dir
, temp_dir
,
505 options
.last_chrome_installer
,
508 # Preferentially copy the files we can find from the output_dir, as
509 # this is where we'll find the Syzygy-optimized executables when
510 # building the optimized mini_installer.
511 if options
.build_dir
!= options
.output_dir
:
512 CopyAllFilesToStagingDir(config
, options
.distribution
,
513 staging_dir
, options
.output_dir
,
514 options
.enable_hidpi
, options
.enable_touch_ui
)
516 # Now copy the remainder of the files from the build dir.
517 CopyAllFilesToStagingDir(config
, options
.distribution
,
518 staging_dir
, options
.build_dir
,
519 options
.enable_hidpi
, options
.enable_touch_ui
)
521 if options
.component_build
== '1':
522 DoComponentBuildTasks(staging_dir
, options
.build_dir
,
523 options
.target_arch
, current_version
)
525 version_numbers
= current_version
.split('.')
526 current_build_number
= version_numbers
[2] + '.' + version_numbers
[3]
527 prev_build_number
= ''
529 version_numbers
= prev_version
.split('.')
530 prev_build_number
= version_numbers
[2] + '.' + version_numbers
[3]
532 # Name of the archive file built (for example - chrome.7z or
533 # patch-<old_version>-<new_version>.7z or patch-<new_version>.7z
534 archive_file
= CreateArchiveFile(options
, staging_dir
,
535 current_build_number
, prev_build_number
)
537 setup_file
= PrepareSetupExec(options
,
538 current_build_number
, prev_build_number
)
540 CreateResourceInputFile(options
.output_dir
, options
.setup_exe_format
,
541 archive_file
, setup_file
, options
.resource_file_path
)
544 parser
= optparse
.OptionParser()
545 parser
.add_option('-i', '--input_file',
546 help='Input file describing which files to archive.')
547 parser
.add_option('-b', '--build_dir',
548 help='Build directory. The paths in input_file are relative to this.')
549 parser
.add_option('--staging_dir',
550 help='Staging directory where intermediate files and directories '
552 parser
.add_option('-o', '--output_dir',
553 help='The output directory where the archives will be written. '
554 'Defaults to the build_dir.')
555 parser
.add_option('--resource_file_path',
556 help='The path where the resource file will be output. '
557 'Defaults to %s in the build directory.' %
558 MINI_INSTALLER_INPUT_FILE
)
559 parser
.add_option('-d', '--distribution',
560 help='Name of Chromium Distribution. Optional.')
561 parser
.add_option('-s', '--skip_rebuild_archive',
562 default
="False", help='Skip re-building Chrome.7z archive if it exists.')
563 parser
.add_option('-l', '--last_chrome_installer',
564 help='Generate differential installer. The value of this parameter '
565 'specifies the directory that contains base versions of '
566 'setup.exe, courgette.exe (if --diff_algorithm is COURGETTE) '
568 parser
.add_option('-f', '--setup_exe_format', default
='COMPRESSED',
569 help='How setup.exe should be included {COMPRESSED|DIFF|FULL}.')
570 parser
.add_option('-a', '--diff_algorithm', default
='BSDIFF',
571 help='Diff algorithm to use when generating differential patches '
572 '{BSDIFF|COURGETTE}.')
573 parser
.add_option('-n', '--output_name', default
='chrome',
574 help='Name used to prefix names of generated archives.')
575 parser
.add_option('--enable_hidpi', default
='0',
576 help='Whether to include HiDPI resource files.')
577 parser
.add_option('--enable_touch_ui', default
='0',
578 help='Whether to include resource files from the "TOUCH" section of the '
580 parser
.add_option('--component_build', default
='0',
581 help='Whether this archive is packaging a component build. This will '
582 'also turn off compression of chrome.7z into chrome.packed.7z and '
583 'helpfully delete any old chrome.packed.7z in |output_dir|.')
584 parser
.add_option('--target_arch', default
='x86',
585 help='Specify the target architecture for installer - this is used '
586 'to determine which CRT runtime files to pull and package '
587 'with the installer archive {x86|x64}.')
589 options
, _
= parser
.parse_args()
590 if not options
.build_dir
:
591 parser
.error('You must provide a build dir.')
593 options
.build_dir
= os
.path
.normpath(options
.build_dir
)
595 if not options
.staging_dir
:
596 parser
.error('You must provide a staging dir.')
598 if not options
.input_file
:
599 parser
.error('You must provide an input file')
601 if not options
.output_dir
:
602 options
.output_dir
= options
.build_dir
604 if not options
.resource_file_path
:
605 options
.resource_file_path
= os
.path
.join(options
.build_dir
,
606 MINI_INSTALLER_INPUT_FILE
)
611 if '__main__' == __name__
:
613 sys
.exit(main(_ParseOptions()))