diff --git a/.hgtags b/.hgtags index cafcdf2516..3f30e0cf0c 100755 --- a/.hgtags +++ b/.hgtags @@ -507,3 +507,4 @@ d40c66e410741de7e90b1ed6dac28dd8a2d7e1f6 3.6.8-release 70eda3721d36df3e00730629c42a1304e5bc65b8 3.6.9-release 5b54b36862ff8bc3b6935673c9d1c1f22ee8d521 3.6.10-release 2feb70a4cfde43f2898d95ff8fcae3e67805c7c2 3.6.11-release +88bbfd7a6971033f3aa103f3a3500ceb4c73521b 3.6.12-release diff --git a/BuildParams b/BuildParams index 31e7e841ad..6b63448c52 100755 --- a/BuildParams +++ b/BuildParams @@ -51,13 +51,13 @@ viewer_channel = "Second Life Test" sourceid = "" additional_packages = "Amazon Desura B C" Amazon_sourceid = "1207v_Amazon" -Amazon_viewer_channel_suffix = " Amazon" +Amazon_viewer_channel_suffix = "Amazon" Desura_sourceid = "1208_desura" -Desura_viewer_channel_suffix = " Desura" +Desura_viewer_channel_suffix = "Desura" B_sourceid = "1301_B" -B_viewer_channel_suffix = " B" +B_viewer_channel_suffix = "B" C_sourceid = "1302_C" -C_viewer_channel_suffix = " C" +C_viewer_channel_suffix = "C" # Report changes since... viewer-development.show_changes_since = last_sprint diff --git a/build.sh b/build.sh index 18a8e7eec0..7baa283829 100755 --- a/build.sh +++ b/build.sh @@ -359,8 +359,7 @@ then if $build_viewer then begin_section Upload Installer - # Upload installer - note that ONLY THE FIRST ITEM uploaded as "installer" - # will appear in the version manager. + # Upload installer package=$(installer_$arch) if [ x"$package" = x ] || test -d "$package" then @@ -384,7 +383,7 @@ then then upload_item installer "$package" binary/octet-stream else - record_failure "Failed to upload $package_id package." + record_failure "Failed to upload $package_id package ($package::$additional_package_name)." fi done export additional_package_name="" diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py index eba0d7abfa..f12ec33358 100755 --- a/indra/lib/python/indra/util/llmanifest.py +++ b/indra/lib/python/indra/util/llmanifest.py @@ -85,7 +85,8 @@ def get_default_platform(dummy): }[sys.platform] DEFAULT_SRCTREE = os.path.dirname(sys.argv[0]) -RELEASE_CHANNEL = 'Firestorm Development' +CHANNEL_VENDOR_BASE = 'Firestorm' +RELEASE_CHANNEL = CHANNEL_VENDOR_BASE + ' Development' ARGUMENTS=[ dict(name='actions', @@ -112,13 +113,14 @@ ARGUMENTS=[ default="Release"), dict(name='dest', description='Destination directory.', default=DEFAULT_SRCTREE), dict(name='grid', - description="""Which grid the client will try to connect to. Even - though it's not strictly a grid, 'firstlook' is also an acceptable - value for this parameter.""", - default=""), + description="""Which grid the client will try to connect to.""", + default=None), dict(name='channel', description="""The channel to use for updates, packaging, settings name, etc.""", default='CHANNEL UNSET'), + dict(name='channel_suffix', + description="""Addition to the channel for packaging and channel value, but not application name (used internally)""", + default=None), dict(name='installer_name', description=""" The name of the file that the installer should be packaged up into. Only used on Linux at the moment.""", @@ -218,9 +220,9 @@ def main(): print "Unable to read versionfile '%s'" % args['versionfile'] raise - # default and agni are default - if args['grid'] in ['default', 'agni']: - args['grid'] = '' + # unspecified, default, and agni are default + if args['grid'] in ['', 'default', 'agni']: + args['grid'] = None if 'actions' in args: args['actions'] = args['actions'].split() @@ -291,21 +293,24 @@ def main(): base_channel_name = args['channel'] # Build each additional package. package_id_list = additional_packages.split(" ") + args['channel'] = base_channel_name for package_id in package_id_list: try: - args['package_id'] = package_id - args['channel'] = base_channel_name + os.environ[package_id + "_viewer_channel_suffix"] + if package_id + "_viewer_channel_suffix" in os.environ: + args['channel_suffix'] = os.environ[package_id + "_viewer_channel_suffix"] + else: + args['channel_suffix'] = None if package_id + "_sourceid" in os.environ: args['sourceid'] = os.environ[package_id + "_sourceid"] else: - args['sourceid'] = "" + args['sourceid'] = None args['dest'] = base_dest_prefix + os.sep + package_id + os.sep + base_dest_postfix except KeyError: sys.stderr.write("Failed to create package for package_id: %s" % package_id) sys.stderr.flush() continue if touch: - print 'Creating additional package for ', package_id, ' in ', args['dest'] + print 'Creating additional package for "', package_id, '" in ', args['dest'] wm = LLManifest.for_platform(args['platform'], args.get('arch'))(args) wm.do(*args['actions']) if touch: @@ -337,7 +342,7 @@ class LLManifest(object): manifests = {} def for_platform(self, platform, arch = None): if arch: - platform = platform + '_' + arch + platform = platform + '_' + arch + '_' return self.manifests[platform.lower()] for_platform = classmethod(for_platform) @@ -354,8 +359,6 @@ class LLManifest(object): self.created_paths = [] self.package_name = "Unknown" - def default_grid(self): - return self.args.get('grid', None) == '' def default_channel(self): return self.args.get('channel', None) == RELEASE_CHANNEL diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index b9ccb8a0e6..9fb817f375 100755 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -2075,6 +2075,7 @@ if (WINDOWS) ARGS ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py --actions=copy + --arch=${ARCH} ${VIEWERMANIFEST_FLAGS} --artwork=${ARTWORK_DIR} --build=${CMAKE_CURRENT_BINARY_DIR} @@ -2143,6 +2144,7 @@ if (WINDOWS) COMMAND ${PYTHON_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py + --arch=${ARCH} ${VIEWERMANIFEST_PACKAGE_FLAGS} --artwork=${ARTWORK_DIR} --build=${CMAKE_CURRENT_BINARY_DIR} @@ -2288,7 +2290,6 @@ if (LINUX) --configuration=${CMAKE_CFG_INTDIR} --dest=${CMAKE_CURRENT_BINARY_DIR}/packaged --grid=${GRID} - --installer_name=${product} --source=${CMAKE_CURRENT_SOURCE_DIR} --touch=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/.${product}.touched --viewer_flavor=${ND_VIEWER_FLAVOR} @@ -2342,7 +2343,7 @@ if (DARWIN) set(MACOSX_BUNDLE_BUNDLE_NAME "Firestorm") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VIEWER_SHORT_VERSION}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${VIEWER_SHORT_VERSION}${VIEWER_MACOSX_PHASE}${VIEWER_REVISION}") - set(MACOSX_BUNDLE_COPYRIGHT "Copyright © The Phoenix Firestorm Project, Inc. 2010-2013") + set(MACOSX_BUNDLE_COPYRIGHT "Copyright ?? The Phoenix Firestorm Project, Inc. 2010-2013") set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "Firestorm.nib") set(MACOSX_BUNDLE_NSPRINCIPAL_CLASS "NSApplication") @@ -2369,6 +2370,7 @@ if (DARWIN) ARGS ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py --actions=copy + --arch=${ARCH} --artwork=${ARTWORK_DIR} --build=${CMAKE_CURRENT_BINARY_DIR} --buildtype=${CMAKE_BUILD_TYPE} @@ -2401,6 +2403,7 @@ if (DARWIN) COMMAND ${PYTHON_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py + --arch=${ARCH} --artwork=${ARTWORK_DIR} --build=${CMAKE_CURRENT_BINARY_DIR} --buildtype=${CMAKE_BUILD_TYPE} diff --git a/indra/newview/fs_viewer_manifest.py b/indra/newview/fs_viewer_manifest.py new file mode 100644 index 0000000000..6e1645bafc --- /dev/null +++ b/indra/newview/fs_viewer_manifest.py @@ -0,0 +1,48 @@ +import subprocess + +class FSViewerManifest: + def fs_is_64bit_build( self ): + return self.args.has_key( 'm64' ) + + def fs_flavor( self ): + return self.args['viewer_flavor'] # [oss or hvk] + + def fs_splice_grid_substitution_strings( self, subst_strings ): + ret = subst_strings + ret[ 'grid' ] = self.args['grid'] + ret[ 'grid_caps' ] = self.args['grid'].upper() + return ret + + def fs_get_substitution_strings( self ): + substitution_strings = { + 'version' : '.'.join(self.args['version']), + 'version_short' : '.'.join(self.args['version'][:-1]), + 'version_dashes' : '-'.join(self.args['version']), + 'channel':self.channel(), + 'channel_oneword':self.channel_oneword(), + 'channel_unique':self.channel_unique(), + 'subchannel_underscores':'_'.join(self.channel_unique().split()), + 'app_name' : self.app_name() + } + + return fs_splice_grid_substitution_strings( substitution_strings ) + + def fs_channel_legacy_oneword(self): + return "".join(self.channel().split()) + + def fs_sign_win_binaries( self ): + try: + subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\firestorm-bin.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) + subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\slplugin.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) + subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\SLVoice.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) + subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\"+self.final_exe()],stderr=subprocess.PIPE,stdout=subprocess.PIPE) + except Exception, e: + print "Couldn't sign final binary. Tried to sign %s" % self.args['configuration']+"\\"+self.final_exe() + + def fs_sign_win_installer( self, substitution_strings ): + try: + subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\"+substitution_strings['installer_file']],stderr=subprocess.PIPE,stdout=subprocess.PIPE) + except Exception, e: + print "Working directory: %s" % os.getcwd() + print "Couldn't sign windows installer. Tried to sign %s" % self.args['configuration']+"\\"+substitution_strings['installer_file'] + diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi index d3a71831b8..7b6f4e4eb8 100755 --- a/indra/newview/installers/windows/installer_template.nsi +++ b/indra/newview/installers/windows/installer_template.nsi @@ -74,16 +74,8 @@ LangString LanguageCode ${LANG_RUSSIAN} "ru" LangString LanguageCode ${LANG_TURKISH} "tr" LangString LanguageCode ${LANG_TRADCHINESE} "zh" -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Tweak for different servers/builds (this placeholder is replaced by viewer_manifest.py) -;; For example: -;; !define INSTFLAGS "%(flags)s" -;; !define INSTNAME "Firestorm%(grid_caps)s" -;; !define SHORTCUT "Firestorm (%(grid_caps)s)" -;; !define URLNAME "secondlife%(grid)s" -;; !define UNINSTALL_SETTINGS 1 - -%%GRID_VARS%% +;; this placeholder is replaced by viewer_manifest.py +%%INST_VARS%% Name ${INSTNAME} @@ -121,7 +113,6 @@ Page instfiles ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Var INSTPROG Var INSTEXE -Var INSTFLAGS Var INSTSHORTCUT Var COMMANDLINE ; command line passed to this installer, set in .onInit Var SHORTCUT_LANG_PARAM ; "--set InstallLanguage de", passes language to viewer @@ -162,7 +153,7 @@ label_ask_launch: label_launch: # Assumes SetOutPath $INSTDIR - Exec '"$INSTDIR\$INSTEXE" $INSTFLAGS $SHORTCUT_LANG_PARAM' + Exec '"$INSTDIR\$INSTEXE" $SHORTCUT_LANG_PARAM' label_no_launch: Pop $R0 FunctionEnd @@ -772,7 +763,6 @@ ShowUninstDetails show Section Uninstall ; Start with some default values. -StrCpy $INSTFLAGS "" StrCpy $INSTPROG "${INSTNAME}" StrCpy $INSTEXE "${INSTEXE}" StrCpy $INSTSHORTCUT "${SHORTCUT}" @@ -891,7 +881,6 @@ Section "" ; (default section) SetShellVarContext all ; install for all users (if you change this, change it in the uninstall as well) ; Start with some default values. -StrCpy $INSTFLAGS "${INSTFLAGS}" StrCpy $INSTPROG "${INSTNAME}" StrCpy $INSTEXE "${INSTEXE}" StrCpy $INSTSHORTCUT "${SHORTCUT}" @@ -941,7 +930,7 @@ StrCmp $NO_STARTMENU "true" label_skip_start_menu CreateDirectory "$SMPROGRAMS\$INSTSHORTCUT" SetOutPath "$INSTDIR" CreateShortCut "$SMPROGRAMS\$INSTSHORTCUT\$INSTSHORTCUT.lnk" \ - "$INSTDIR\$INSTEXE" "$INSTFLAGS $SHORTCUT_LANG_PARAM" + "$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" WriteINIStr "$SMPROGRAMS\$INSTSHORTCUT\SL Create Account.url" \ @@ -964,9 +953,9 @@ label_skip_start_menu: ; Other shortcuts SetOutPath "$INSTDIR" CreateShortCut "$DESKTOP\$INSTSHORTCUT.lnk" \ - "$INSTDIR\$INSTEXE" "$INSTFLAGS $SHORTCUT_LANG_PARAM" + "$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" CreateShortCut "$INSTDIR\$INSTSHORTCUT.lnk" \ - "$INSTDIR\$INSTEXE" "$INSTFLAGS $SHORTCUT_LANG_PARAM" + "$INSTDIR\$INSTEXE" "$SHORTCUT_LANG_PARAM" CreateShortCut "$INSTDIR\Uninstall $INSTSHORTCUT.lnk" \ '"$INSTDIR\uninst.exe"' '' @@ -975,7 +964,6 @@ CreateShortCut "$INSTDIR\Uninstall $INSTSHORTCUT.lnk" \ ; Write registry WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\The Phoenix Firestorm Project\$INSTPROG" "" "$INSTDIR" WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\The Phoenix Firestorm Project\$INSTPROG" "Version" "${VERSION_LONG}" -WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\The Phoenix Firestorm Project\$INSTPROG" "Flags" "$INSTFLAGS" WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\The Phoenix Firestorm Project\$INSTPROG" "Shortcut" "$INSTSHORTCUT" WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\The Phoenix Firestorm Project\$INSTPROG" "Exe" "$INSTEXE" WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\$INSTPROG" "DisplayName" "$INSTPROG (remove only)" @@ -1000,19 +988,19 @@ WriteRegStr HKEY_CLASSES_ROOT "${URLNAME}" "URL Protocol" "" WriteRegStr HKEY_CLASSES_ROOT "${URLNAME}\DefaultIcon" "" '"$INSTDIR\$INSTEXE"' ;; URL param must be last item passed to viewer, it ignores subsequent params ;; to avoid parameter injection attacks. -WriteRegExpandStr HKEY_CLASSES_ROOT "${URLNAME}\shell\open\command" "" '"$INSTDIR\$INSTEXE" $INSTFLAGS -url "%1"' +WriteRegExpandStr HKEY_CLASSES_ROOT "${URLNAME}\shell\open\command" "" '"$INSTDIR\$INSTEXE" -url "%1"' WriteRegStr HKEY_CLASSES_ROOT "x-grid-location-info"(default)" "URL:Second Life" WriteRegStr HKEY_CLASSES_ROOT "x-grid-location-info" "URL Protocol" "" WriteRegStr HKEY_CLASSES_ROOT "x-grid-location-info\DefaultIcon" "" '"$INSTDIR\$INSTEXE"' ;; URL param must be last item passed to viewer, it ignores subsequent params ;; to avoid parameter injection attacks. -WriteRegExpandStr HKEY_CLASSES_ROOT "x-grid-location-info\shell\open\command" "" '"$INSTDIR\$INSTEXE" $INSTFLAGS -url "%1"' +WriteRegExpandStr HKEY_CLASSES_ROOT "x-grid-location-info\shell\open\command" "" '"$INSTDIR\$INSTEXE" -url "%1"' ; Register hop:// protocol registry info WriteRegStr HKEY_CLASSES_ROOT "hop" "(default)" "URL:Second Life" WriteRegStr HKEY_CLASSES_ROOT "hop" "URL Protocol" "" WriteRegStr HKEY_CLASSES_ROOT "hop\DefaultIcon" "" '"$INSTDIR\$INSTEXE"' -WriteRegExpandStr HKEY_CLASSES_ROOT "hop\shell\open\command" "" '"$INSTDIR\$INSTEXE" $INSTFLAGS -url "%1"' +WriteRegExpandStr HKEY_CLASSES_ROOT "hop\shell\open\command" "" '"$INSTDIR\$INSTEXE" -url "%1"' ; ; write out uninstaller diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 241aef8a00..dfa0be63aa 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -40,18 +40,20 @@ import subprocess import zipfile #/AO +from fs_viewer_manifest import FSViewerManifest # Manifest extensions for Firestorm + viewer_dir = os.path.dirname(__file__) # Add indra/lib/python to our path so we don't have to muck with PYTHONPATH. # Put it FIRST because some of our build hosts have an ancient install of # indra.util.llmanifest under their system Python! sys.path.insert(0, os.path.join(viewer_dir, os.pardir, "lib", "python")) -from indra.util.llmanifest import LLManifest, main, proper_windows_path, path_ancestors +from indra.util.llmanifest import LLManifest, main, proper_windows_path, path_ancestors, CHANNEL_VENDOR_BASE, RELEASE_CHANNEL try: from llbase import llsd except ImportError: from indra.base import llsd -class ViewerManifest(LLManifest): +class ViewerManifest(LLManifest,FSViewerManifest): def is_packaging_viewer(self): # Some commands, files will only be included # if we are packaging the viewer on windows. @@ -141,21 +143,29 @@ class ViewerManifest(LLManifest): Persist=1, Type='String', Value=''), + CmdLineGridChoice=dict(Comment='Default grid', + Persist=0, + Type='String', + Value=''), CmdLineChannel=dict(Comment='Command line specified channel name', Persist=0, Type='String', Value='')) settings_install = {} - for key, setting in (("sourceid", "sourceid"), - ("channel", "CmdLineChannel")): - if key in self.args: - # only set if value is non-empty - if self.args[key]: - # copy corresponding setting from settings_template - settings_install[setting] = settings_template[setting].copy() - # then fill in Value - settings_install[setting]["Value"] = self.args[key] - print "Put %s '%s' in settings_install.xml" % (setting, self.args[key]) + if 'sourceid' in self.args and self.args['sourceid']: + settings_install['sourceid'] = settings_template['sourceid'].copy() + settings_install['sourceid']['Value'] = self.args['sourceid'] + print "Set sourceid in settings_install.xml to '%s'" % self.args['sourceid'] + + if 'channel_suffix' in self.args and self.args['channel_suffix']: + settings_install['CmdLineChannel'] = settings_template['CmdLineChannel'].copy() + settings_install['CmdLineChannel']['Value'] = self.channel_with_pkg_suffix() + print "Set CmdLineChannel in settings_install.xml to '%s'" % self.channel_with_pkg_suffix() + + if 'grid' in self.args and self.args['grid']: + settings_install['CmdLineGridChoice'] = settings_template['CmdLineGridChoice'].copy() + settings_install['CmdLineGridChoice']['Value'] = self.grid() + print "Set CmdLineGridChoice in settings_install.xml to '%s'" % self.grid() # did we actually copy anything into settings_install dict? if settings_install: @@ -261,78 +271,88 @@ class ViewerManifest(LLManifest): def grid(self): return self.args['grid'] + def channel(self): return self.args['channel'] - def channel_unique(self): - return self.channel().replace("Firestorm", "").strip() - def channel_legacy_oneword(self): - return "".join(self.channel().split()) - def channel_oneword(self): - return "".join(self.channel_unique().split()) - def channel_lowerword(self): - return self.channel_oneword().lower() - def flavor(self): # Viewer Flavor [FS:CR] - return self.args['viewer_flavor'] # [oss or hvk] + + def channel_with_pkg_suffix(self): + fullchannel=self.channel() + if 'channel_suffix' in self.args and self.args['channel_suffix']: + fullchannel+=' '+self.args['channel_suffix'] + return fullchannel + + def channel_variant(self): + global CHANNEL_VENDOR_BASE + return self.channel().replace(CHANNEL_VENDOR_BASE, "").strip() + + def channel_type(self): # returns 'release', 'beta', 'project', or 'test' + global CHANNEL_VENDOR_BASE + channel_qualifier=self.channel().replace(CHANNEL_VENDOR_BASE, "").lower().strip() + if channel_qualifier.startswith('release'): + channel_type='release' + elif channel_qualifier.startswith('beta'): + channel_type='beta' + elif channel_qualifier.startswith('project'): + channel_type='project' + else: + channel_type='test' + return channel_type + + def channel_variant_app_suffix(self): + # get any part of the compiled channel name after the CHANNEL_VENDOR_BASE + suffix=self.channel_variant() + # by ancient convention, we don't use Release in the app name + if self.channel_type() == 'release': + suffix=suffix.replace('Release', '').strip() + # for the base release viewer, suffix will now be null - for any other, append what remains + if len(suffix) > 0: + suffix = "_"+ ("_".join(suffix.split())) + # the additional_packages mechanism adds more to the installer name (but not to the app name itself) + if 'channel_suffix' in self.args and self.args['channel_suffix']: + suffix+='_'+("_".join(self.args['channel_suffix'].split())) + return suffix + + def installer_base_name(self): + global CHANNEL_VENDOR_BASE + # a standard map of strings for replacing in the templates + substitution_strings = { + 'channel_vendor_base' : '_'.join(CHANNEL_VENDOR_BASE.split()), + 'channel_variant_underscores':self.channel_variant_app_suffix(), + 'version_underscores' : '_'.join(self.args['version']), + 'arch':self.args['arch'] + } + return "%(channel_vendor_base)s%(channel_variant_underscores)s_%(version_underscores)s_%(arch)s" % substitution_strings def app_name(self): - # [FS:CR] - #app_suffix='Test' - #channel_type=self.channel_lowerword() - #if channel_type.startswith('release') : - # app_suffix='Viewer' - #elif re.match('^(beta|project).*',channel_type) : - # app_suffix=self.channel_unique() - #return "Second Life "+app_suffix - app = 'Firestorm' - if (self.flavor() == 'oss') : - app = 'FirestormOS' - app_suffix = ''.join(self.channel_unique().split()) - return app + app_suffix - # [/FS:CR] + global CHANNEL_VENDOR_BASE + channel_type=self.channel_type() + if channel_type == 'release': + app_suffix='Viewer' + else: + app_suffix=self.channel_variant() + + # tag "OS" after CHANNEL_VENDOR_BASE and before any suffix + if self.fs_flavor() == 'oss': + app_suffix = "OS" + app_suffix + # + + # Don't separate name by whitespace. This break a lot of things in the old FS installer logic. + #return CHANNEL_VENDOR_BASE + ' ' + app_suffix + return CHANNEL_VENDOR_BASE + app_suffix + # + + def app_name_oneword(self): + return ''.join(self.app_name().split()) + def icon_path(self): - icon_path="icons/" - channel_type=self.channel_lowerword() - print "Icon channel type '%s'" % channel_type - if channel_type.startswith('release') : - icon_path += 'release' - elif re.match('^beta.*',channel_type) : - icon_path += 'beta' - elif re.match('^project.*',channel_type) : - icon_path += 'project' - else : - icon_path += 'private' # FS default - #[FS:CR] OpenSim app icons - if (self.flavor() == 'oss') : - icon_path += '-os' - # [/FS:CR] - return icon_path - - def flags_list(self): - """ Convenience function that returns the command-line flags - for the grid""" - - # The original role of this method seems to have been to build a - # grid-specific viewer: one that would, on launch, preselect a - # particular grid. (Apparently that dates back to when the protocol - # between viewer and simulator required them to be updated in - # lockstep, so that "the beta grid" required "a beta viewer.") But - # those viewer command-line switches no longer work without tweaking - # user_settings/grids.xml. In fact, going forward, it's unclear what - # use case that would address. - - # This method also set a channel-specific (or grid-and-channel- - # specific) user_settings/settings_something.xml file. It has become - # clear that saving user settings in a channel-specific file causes - # more problems (confusion) than it solves, so we've discontinued that. - - # In fact we now avoid forcing viewer command-line switches at all, - # instead introducing a settings_install.xml file. Command-line - # switches don't aggregate well; for instance the generated --channel - # switch actually prevented the user specifying --channel on the - # command line. Settings files have well-defined override semantics. - return None + # Add -os for oss builds + if self.fs_flavor() == 'oss': + return "icons/" + self.channel_type() + "-os" + # + return "icons/" + self.channel_type() + def extract_names(self,src): try: contrib_file = open(src,'r') @@ -356,22 +376,9 @@ class ViewerManifest(LLManifest): random.shuffle(names) return ', '.join(names) -class WindowsManifest(ViewerManifest): +class Windows_i686_Manifest(ViewerManifest): def final_exe(self): - # [FS:CR] - #app_suffix="Test" - #channel_type=self.channel_lowerword() - #if channel_type.startswith('release') : - # app_suffix='' - #elif re.match('^(beta|project).*',channel_type) : - # app_suffix=''.join(self.channel_unique().split()) - #return "SecondLife"+app_suffix+".exe" - app = 'Firestorm' - if (self.flavor() == 'oss') : - app = 'FirestormOS' - app_suffix = ''.join(self.channel_unique().split()) - return app + app_suffix + ".exe" - # [/FS:CR] + return self.app_name_oneword()+".exe" def test_msvcrt_and_copy_action(self, src, dst): # This is used to test a dll manifest. @@ -420,7 +427,7 @@ class WindowsManifest(ViewerManifest): print "Doesn't exist:", src def construct(self): - super(WindowsManifest, self).construct() + super(Windows_i686_Manifest, self).construct() if self.is_packaging_viewer(): # Find secondlife-bin.exe in the 'configuration' dir, then rename it to the result of final_exe. @@ -696,87 +703,47 @@ class WindowsManifest(ViewerManifest): 'version_short' : '.'.join(self.args['version'][:-1]), 'version_dashes' : '-'.join(self.args['version']), 'final_exe' : self.final_exe(), - 'grid':self.args['grid'], - 'grid_caps':self.args['grid'].upper(), 'flags':'', - 'channel':self.channel(), - 'channel_oneword':self.channel_oneword(), - 'channel_unique':self.channel_unique(), - 'subchannel_underscores':'_'.join(self.channel_unique().split()), - 'app_name' : self.app_name() #[FS:CR] + 'app_name':self.app_name(), + 'app_name_oneword':self.app_name_oneword() } + substitution_strings = self.fs_splice_grid_substitution_strings( substitution_strings ) # Add grid args + + installer_file = self.installer_base_name() + '_Setup.exe' + substitution_strings['installer_file'] = installer_file + version_vars = """ !define INSTEXE "%(final_exe)s" !define VERSION "%(version_short)s" !define VERSION_LONG "%(version)s" !define VERSION_DASHES "%(version_dashes)s" """ % substitution_strings - if self.default_channel(): - if self.default_grid(): - # release viewer - installer_file = "Phoenix-%(app_name)s-%(version_dashes)s_Setup.exe" - grid_vars_template = """ - OutFile "%(installer_file)s" - !define INSTFLAGS "%(flags)s" - !define INSTNAME "%(app_name)s" - !define SHORTCUT "%(app_name)s" - !define URLNAME "secondlife" - Caption "%(app_name)s ${VERSION}" - """ - else: - # alternate grid viewer - installer_file = "Phoenix-%(app_name)s-%(version_dashes)s_(%(grid_caps)s)_Setup.exe" - grid_vars_template = """ - OutFile "%(installer_file)s" - !define INSTFLAGS "%(flags)s" - !define INSTNAME "%(app_name)s%(grid_caps)s" - !define SHORTCUT "%(app_name)s (%(grid_caps)s)" - !define URLNAME "secondlife%(grid)s" - !define UNINSTALL_SETTINGS 1 - Caption "%(app_name)s %(grid)s ${VERSION}" - """ + + if self.channel_type() == 'release': + substitution_strings['caption'] = CHANNEL_VENDOR_BASE else: - # some other channel (grid name not used) - #installer_file = "Second_Life_%(version_dashes)s_%(subchannel_underscores)s_Setup.exe" - installer_file = "Phoenix-%(app_name)s-%(version_dashes)s_Setup.exe" # - grid_vars_template = """ + substitution_strings['caption'] = self.app_name() + ' ${VERSION}' + + inst_vars_template = """ OutFile "%(installer_file)s" - !define INSTFLAGS "%(flags)s" - !define INSTNAME "%(app_name)s" + !define INSTNAME "%(app_name_oneword)s" !define SHORTCUT "%(app_name)s" !define URLNAME "secondlife" - !define UNINSTALL_SETTINGS 1 - Caption "%(app_name)s ${VERSION}" + Caption "%(caption)s" """ - if 'installer_name' in self.args: - installer_file = self.args['installer_name'] - else: - installer_file = installer_file % substitution_strings - if len(self.args['package_id']) > 0: - installer_file = installer_file.replace(self.args['package_id'], "") - installer_file = installer_file.replace(".exe", self.args['package_id'] + ".exe") - substitution_strings['installer_file'] = installer_file tempfile = "secondlife_setup_tmp.nsi" - #AO: Try to sign original executable first, if we can, using best available signing cert. - try: - subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\firestorm-bin.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) - subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\slplugin.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) - subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\SLVoice.exe"],stderr=subprocess.PIPE,stdout=subprocess.PIPE) - subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\"+self.final_exe()],stderr=subprocess.PIPE,stdout=subprocess.PIPE) - except Exception, e: - print "Couldn't sign final binary. Tried to sign %s" % self.args['configuration']+"\\"+self.final_exe() - + self.fs_sign_win_binaries() # Sign files, step one. Sign compiled binaries - if not self.is_64bit_build(): + if not self.fs_is_64bit_build(): # the following replaces strings in the nsi template # it also does python-style % substitution self.replace_in("installers/windows/installer_template.nsi", tempfile, { "%%VERSION%%":version_vars, "%%SOURCE%%":self.get_src_prefix(), - "%%GRID_VARS%%":grid_vars_template % substitution_strings, + "%%INST_VARS%%":inst_vars_template % substitution_strings, "%%INSTALL_FILES%%":self.nsi_file_commands(True), "%%DELETE_FILES%%":self.nsi_file_commands(False)}) @@ -800,12 +767,8 @@ class WindowsManifest(ViewerManifest): " " + substitution_strings[ 'channel' ] + " " + substitution_strings[ 'version' ] + " " + settingsFile + " " + installer_file + " " + " ".join( substitution_strings[ 'version' ].split(".") ) ) - #AO: Try to sign installer next, if we can, using "The Phoenix Firestorm Project" signing cert. - try: - subprocess.check_call(["signtool.exe","sign","/n","Phoenix","/d","Firestorm","/du","http://www.phoenixviewer.com",self.args['configuration']+"\\"+substitution_strings['installer_file']],stderr=subprocess.PIPE,stdout=subprocess.PIPE) - except Exception, e: - print "Working directory: %s" % os.getcwd() - print "Couldn't sign windows installer. Tried to sign %s" % self.args['configuration']+"\\"+substitution_strings['installer_file'] + + self.fs_sign_win_installer( substitution_strings ) # Sign files, step two. Sign installer. #AO: Try to package up symbols # New Method, for reading cross platform stack traces on a linux/mac host @@ -855,7 +818,7 @@ class WindowsManifest(ViewerManifest): self.package_file = installer_file -class DarwinManifest(ViewerManifest): +class Darwin_i386_Manifest(ViewerManifest): def is_packaging_viewer(self): # darwin requires full app bundle packaging even for debugging. return True @@ -880,7 +843,7 @@ class DarwinManifest(ViewerManifest): # most everything goes in the Resources directory if self.prefix(src="", dst="Resources"): - super(DarwinManifest, self).construct() + super(Darwin_i386_Manifest, self).construct() if self.prefix("cursors_mac"): self.path("*.tif") @@ -1022,21 +985,8 @@ class DarwinManifest(ViewerManifest): self.run_command("chmod +x %r" % os.path.join(self.get_dst_prefix(), script)) def package_finish(self): - # Copied from windows manifest, since we're starting to use many of the same vars - # a standard map of strings for replacing in the templates - substitution_strings = { - 'version' : '.'.join(self.args['version']), - 'version_short' : '.'.join(self.args['version'][:-1]), - 'version_dashes' : '-'.join(self.args['version']), - 'grid':self.args['grid'], - 'grid_caps':self.args['grid'].upper(), - 'channel':self.channel(), - 'channel_oneword':self.channel_oneword(), - 'channel_unique':self.channel_unique(), - 'subchannel_underscores':'_'.join(self.channel_unique().split()), - 'app_name' : self.app_name() #[FS:CR] - } - # + global CHANNEL_VENDOR_BASE + substitution_strings = self.fs_get_substitution_strings() # Copied from windows manifest, since we're starting to use many of the same vars #Comment out for now. FS:TM #Added from LL signing for OSX 10.8 @@ -1069,19 +1019,11 @@ class DarwinManifest(ViewerManifest): # MBW -- If the mounted volume name changes, it breaks the .DS_Store's background image and icon positioning. # If we really need differently named volumes, we'll need to create multiple DS_Store file images, or use some other trick. - #volname="Firestorm Installer" # DO NOT CHANGE without understanding comment above + #volname=CHANNEL_VENDOR_BASE+" Installer" # DO NOT CHANGE without understanding comment above #[FS:CR] Understood and disregarded! volname = (self.app_name() + " " + '.'.join(self.args['version']) + " Installer") - #if len(self.args['package_id']) > 0: - # imagename = imagename + self.args['package_id'] - #elif self.default_channel(): - # if not self.default_grid(): - # # beta case - # imagename = imagename + '_' + self.args['grid'].upper() - #else: - # # first look, etc - # imagename = imagename + '_' + self.channel_oneword().upper() + #imagename = self.installer_base_name() sparsename = imagename + ".sparseimage" finalname = imagename + ".dmg" @@ -1233,8 +1175,9 @@ class LinuxManifest(ViewerManifest): # recurse self.end_prefix("res-sdl") - # Get the icons based on the channel + # Get the icons based on the channel type icon_path = self.icon_path() + print "DEBUG: icon_path '%s'" % icon_path if self.prefix(src=icon_path, dst="") : self.path("firestorm_256.png","firestorm_48.png") if self.prefix(src="",dst="res-sdl") : @@ -1261,33 +1204,10 @@ class LinuxManifest(ViewerManifest): def package_finish(self): # a standard map of strings for replacing in the templates - #installer_name_components = ['Phoenix',self.channel_oneword(),self.args.get('arch'),'.'.join(self.args['version'])] installer_name_components = ['Phoenix',self.app_name(),self.args.get('arch'),'.'.join(self.args['version'])] installer_name = "_".join(installer_name_components) - - # Copied from windows manifest, since we're starting to use many of the same vars - # a standard map of strings for replacing in the templates - substitution_strings = { - 'version' : '.'.join(self.args['version']), - 'version_short' : '.'.join(self.args['version'][:-1]), - 'version_dashes' : '-'.join(self.args['version']), - 'grid':self.args['grid'], - 'grid_caps':self.args['grid'].upper(), - 'channel':self.channel(), - 'channel_oneword':self.channel_oneword(), - 'channel_unique':self.channel_unique(), - 'subchannel_underscores':'_'.join(self.channel_unique().split()), - 'app_name' : self.app_name() #[FS:CR] - } - # - - #if self.default_channel(): - # if not self.default_grid(): - # installer_name += '_' + self.args['grid'].upper() - #else: - # installer_name += '_' + self.channel_oneword().upper() - print "installer name=%s" % installer_name - + #installer_name = self.installer_base_name() + self.strip_binaries() # Fix access permissions @@ -1339,9 +1259,9 @@ class LinuxManifest(ViewerManifest): self.args['viewer_flavor'] ) ) -class Linux_i686Manifest(LinuxManifest): +class Linux_i686_Manifest(LinuxManifest): def construct(self): - super(Linux_i686Manifest, self).construct() + super(Linux_i686_Manifest, self).construct() if self.prefix("../packages/lib/release", dst="lib"): self.path("libapr-1.so") @@ -1438,9 +1358,9 @@ class Linux_i686Manifest(LinuxManifest): self.strip_binaries() -class Linux_x86_64Manifest(LinuxManifest): +class Linux_x86_64_Manifest(LinuxManifest): def construct(self): - super(Linux_x86_64Manifest, self).construct() + super(Linux_x86_64_Manifest, self).construct() # support file for valgrind debug tool self.path("secondlife-i686.supp") diff --git a/indra/viewer_components/updater/llupdatedownloader.cpp b/indra/viewer_components/updater/llupdatedownloader.cpp index c28ad76c77..c42112af80 100755 --- a/indra/viewer_components/updater/llupdatedownloader.cpp +++ b/indra/viewer_components/updater/llupdatedownloader.cpp @@ -77,7 +77,8 @@ private: void run(void); void startDownloading(LLURI const & uri, std::string const & hash); void throwOnCurlError(CURLcode code); - bool validateDownload(void); + bool validateDownload(const std::string& filePath); + bool validateOrRemove(const std::string& filePath); LOG_CLASS(LLUpdateDownloader::Implementation); }; @@ -295,9 +296,8 @@ void LLUpdateDownloader::Implementation::resume(void) { resumeDownloading(fileStatus.st_size); } - else if(!validateDownload()) + else if(!validateOrRemove(filePath)) { - LLFile::remove(filePath); download(LLURI(mDownloadData["url"].asString()), mDownloadData["hash"].asString(), mDownloadData["update_channel"].asString(), @@ -421,19 +421,13 @@ void LLUpdateDownloader::Implementation::run(void) if(code == CURLE_OK) { LLFile::remove(mDownloadRecordPath); - if(validateDownload()) + if(validateOrRemove(mDownloadData["path"])) { LL_INFOS("UpdaterService") << "download successful" << LL_ENDL; mClient.downloadComplete(mDownloadData); } else { - LL_INFOS("UpdaterService") << "download failed hash check" << LL_ENDL; - std::string filePath = mDownloadData["path"].asString(); - if(filePath.size() != 0) - { - LLFile::remove(filePath); - } mClient.downloadError("failed hash check"); } } @@ -449,7 +443,9 @@ void LLUpdateDownloader::Implementation::run(void) LLFile::remove(mDownloadRecordPath); if(mDownloadData.has("path")) { - LLFile::remove(mDownloadData["path"].asString()); + std::string filePath = mDownloadData["path"].asString(); + LL_INFOS("UpdaterService") << "removing " << filePath << LL_ENDL; + LLFile::remove(filePath); } mClient.downloadError("curl error"); } @@ -561,31 +557,49 @@ void LLUpdateDownloader::Implementation::throwOnCurlError(CURLcode code) } } - -bool LLUpdateDownloader::Implementation::validateDownload(void) +bool LLUpdateDownloader::Implementation::validateOrRemove(const std::string& filePath) +{ + bool valid = validateDownload(filePath); + if (! valid) + { + LL_INFOS("UpdaterService") << "removing " << filePath << LL_ENDL; + LLFile::remove(filePath); + } + return valid; +} + +bool LLUpdateDownloader::Implementation::validateDownload(const std::string& filePath) { - std::string filePath = mDownloadData["path"].asString(); llifstream fileStream(filePath, std::ios_base::in | std::ios_base::binary); if(!fileStream) { + LL_INFOS("UpdaterService") << "can't open " << filePath << ", invalid" << LL_ENDL; return false; } std::string hash = mDownloadData["hash"].asString(); - if(hash.size() != 0) + if (! hash.empty()) { - LL_INFOS("UpdaterService") << "checking hash..." << LL_ENDL; char digest[33]; LLMD5(fileStream).hex_digest(digest); - if(hash != digest) + if (hash == digest) { - LL_WARNS("UpdaterService") << "download hash mismatch; expected " << hash << - " but download is " << digest << LL_ENDL; + LL_INFOS("UpdaterService") << "verified hash " << hash + << " for downloaded " << filePath << LL_ENDL; + return true; + } + else + { + LL_WARNS("UpdaterService") << "download hash mismatch for " + << filePath << ": expected " << hash + << " but computed " << digest << LL_ENDL; + return false; } - return hash == digest; } else { + LL_INFOS("UpdaterService") << "no hash specified for " << filePath + << ", unverified" << LL_ENDL; return true; // No hash check provided. } } diff --git a/indra/viewer_components/updater/llupdaterservice.cpp b/indra/viewer_components/updater/llupdaterservice.cpp index 7e82fe4908..d3257d4f21 100755 --- a/indra/viewer_components/updater/llupdaterservice.cpp +++ b/indra/viewer_components/updater/llupdaterservice.cpp @@ -296,37 +296,49 @@ bool LLUpdaterServiceImpl::checkForInstall(bool launchInstaller) update_marker.close(); // Get the path to the installer file. - LLSD path = update_info.get("path"); - if(update_info["current_version"].asString() != ll_get_version()) + std::string path(update_info.get("path")); + std::string downloader_version(update_info["current_version"]); + if (downloader_version != ll_get_version()) { // This viewer is not the same version as the one that downloaded - // the update. Do not install this update. - if(!path.asString().empty()) + // the update. Do not install this update. + LL_INFOS("UpdaterService") << "ignoring update downloaded by " + << "different viewer version " + << downloader_version << LL_ENDL; + if (! path.empty()) { - LL_INFOS("UpdaterService") << "ignoring update dowloaded by different client version" << LL_ENDL;; - LLFile::remove(path.asString()); + LL_INFOS("UpdaterService") << "removing " << path << LL_ENDL; + LLFile::remove(path); LLFile::remove(update_marker_path()); } - else - { - ; // Nothing to clean up. - } - + foundInstall = false; } - else if(path.isDefined() && !path.asString().empty()) + else if (path.empty()) + { + LL_WARNS("UpdaterService") << "Marker file " << update_marker_path() + << " 'path' entry empty, ignoring" << LL_ENDL; + foundInstall = false; + } + else if (! LLFile::isfile(path)) + { + LL_WARNS("UpdaterService") << "Nonexistent installer " << path + << ", ignoring" << LL_ENDL; + foundInstall = false; + } + else { if(launchInstaller) { setState(LLUpdaterService::INSTALLING); - + LLFile::remove(update_marker_path()); int result = ll_install_update(install_script_path(), - update_info["path"].asString(), + path, update_info["required"].asBoolean(), install_script_mode()); - + if((result == 0) && mAppExitCallback) { mAppExitCallback(); @@ -360,7 +372,8 @@ bool LLUpdaterServiceImpl::checkForResume() LLSD download_info; LLSDSerialize::fromXMLDocument(download_info, download_marker_stream); download_marker_stream.close(); - if(download_info["current_version"].asString() == ll_get_version()) + std::string downloader_version(download_info["current_version"]); + if (downloader_version == ll_get_version()) { mIsDownloading = true; mNewVersion = download_info["update_version"].asString(); @@ -371,10 +384,13 @@ bool LLUpdaterServiceImpl::checkForResume() else { // The viewer that started this download is not the same as this viewer; ignore. - LL_INFOS("UpdaterService") << "ignoring partial download from different viewer version" << LL_ENDL;; + LL_INFOS("UpdaterService") << "ignoring partial download " + << "from different viewer version " + << downloader_version << LL_ENDL; std::string path = download_info["path"].asString(); if(!path.empty()) { + LL_INFOS("UpdaterService") << "removing " << path << LL_ENDL; LLFile::remove(path); } LLFile::remove(download_marker_path); @@ -539,7 +555,7 @@ bool LLUpdaterServiceImpl::onMainLoop(LLSD const & event) // Check for failed install. if(LLFile::isfile(ll_install_failed_marker_path())) { - LL_DEBUGS("UpdaterService") << "found marker " << ll_install_failed_marker_path() << LL_ENDL;; + LL_DEBUGS("UpdaterService") << "found marker " << ll_install_failed_marker_path() << LL_ENDL; int requiredValue = 0; { llifstream stream(ll_install_failed_marker_path()); @@ -552,12 +568,12 @@ bool LLUpdaterServiceImpl::onMainLoop(LLSD const & event) // TODO: notify the user. LL_WARNS("UpdaterService") << "last install attempt failed" << LL_ENDL;; LLFile::remove(ll_install_failed_marker_path()); - + LLSD event; event["type"] = LLSD(LLUpdaterService::INSTALL_ERROR); event["required"] = LLSD(requiredValue); LLEventPumps::instance().obtain(LLUpdaterService::pumpName()).post(event); - + setState(LLUpdaterService::TERMINAL); } // 3.6.4 check this, commented out to compile diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py index 4a38214d60..32da1ed05d 100755 --- a/indra/viewer_components/updater/scripts/darwin/update_install.py +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -201,6 +201,11 @@ def main(dmgfile, markerfile, markertext): # prepare for other cleanup with Janitor(LOGF) as janitor: + # Under some circumstances, this script seems to be invoked with a + # nonexistent pathname. Check for that. + if not os.path.isfile(dmgfile): + fail(dmgfile + " has been deleted") + # Try to derive the name of the running viewer app bundle from our # own pathname. (Hopefully the old viewer won't copy this script # to a temp dir before running!) @@ -378,6 +383,13 @@ def main(dmgfile, markerfile, markertext): log(' '.join(command)) subprocess.check_call(command, stdout=LOGF, stderr=subprocess.STDOUT) + # If all the above succeeded, delete the .dmg file. We don't do this + # as a janitor.later() operation because we only want to do it if we + # get this far successfully. Note that this is out of the scope of the + # Janitor: we must detach the .dmg before removing it! + log("rm " + dmgfile) + os.remove(dmgfile) + except Exception, err: # Because we carefully set sys.excepthook -- and even modify it to log # the problem once we have our log file open -- you might think we