'From Squeak3.6alpha of ''17 March 2003'' [latest update: #5247] on 9 June 2003 at 6:26:53 pm'! "Change Set: SARInstallerFor34-nk Date: 23 March 2003 Author: Ned Konz Support for installing SAR (Squeak ARchive) files. A SAR file stores multiple files in a single .zip-formatted file. (This includes a postscript which registers this version of 'SARInstaller for 3.4' (15) as installed. -dew) " ! Model subclass: #SARInstaller instanceVariableNames: 'zip directory fileName ' classVariableNames: '' poolDictionaries: '' category: 'SARInstaller'! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:57'! addDirectory: aFileName as: anotherFileName | newMember | newMember _ self memberClass newFromDirectory: aFileName. self addMember: newMember. newMember localFileName: anotherFileName. ^newMember! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 15:03'! addFile: aFileName as: anotherFileName | newMember | newMember _ self memberClass newFromFile: aFileName. self addMember: newMember. newMember localFileName: anotherFileName. ^newMember! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 15:03'! addString: aString as: aFileName | newMember | newMember _ self memberClass newFromString: aString named: aFileName. self addMember: newMember. newMember localFileName: aFileName. ^newMember! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 15:03'! addTree: aFileNameOrDirectory removingFirstCharacters: n | dir newMember fullPath relativePath | dir _ (aFileNameOrDirectory isString) ifTrue: [ FileDirectory on: aFileNameOrDirectory ] ifFalse: [ aFileNameOrDirectory ]. fullPath _ dir fullNameFor: ''. "this could be a bug..." relativePath _ fullPath copyFrom: n + 1 to: fullPath size. dir entries do: [ :ea | | fullName | fullName _ fullPath, ea name. newMember _ ea isDirectory ifTrue: [ self memberClass newFromDirectory: fullName ] ifFalse: [ self memberClass newFromFile: fullName ]. newMember localFileName: relativePath, ea name. self addMember: newMember. ea isDirectory ifTrue: [ self addTree: fullName removingFirstCharacters: n ]. ]. ! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:48'! extractMember: aMemberOrName | member | member _ self member: aMemberOrName. member ifNil: [ ^nil ]. member extractToFileNamed: member localFileName inDirectory: FileDirectory default.! ! !Archive methodsFor: 'archive operations' stamp: 'nk 11/11/2002 14:09'! extractMemberWithoutPath: aMemberOrName self extractMemberWithoutPath: aMemberOrName inDirectory: FileDirectory default.! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:48'! extractMemberWithoutPath: aMemberOrName inDirectory: dir | member | member _ self member: aMemberOrName. member ifNil: [ ^nil ]. member extractToFileNamed: (FileDirectory localNameFor: member localFileName) inDirectory: dir! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:50'! memberNamed: aString "Return the first member whose zip name or local file name matches aString, or nil" ^members detect: [ :ea | ea fileName = aString or: [ ea localFileName = aString ]] ifNone: [ ]! ! !Archive methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:50'! membersMatching: aString ^members select: [ :ea | (aString match: ea fileName) or: [ aString match: ea localFileName ] ]! ! !ArchiveMember methodsFor: 'accessing' stamp: 'nk 12/20/2002 15:02'! localFileName: aString "Set my internal filename. Returns the (possibly new) filename. aString will be translated from local FS format into Unix format." ^fileName _ aString copyReplaceAll: FileDirectory slash with: '/'.! ! !ArchiveMember methodsFor: 'printing' stamp: 'nk 12/20/2002 15:11'! printOn: aStream super printOn: aStream. aStream nextPut: $(; nextPutAll: self fileName; nextPut: $)! ! !ArchiveViewer methodsFor: 'archive operations' stamp: 'nk 11/11/2002 22:42'! extractAll "Extracts all in a directory of the users' choosing." | directory | self canExtractAll ifFalse: [^ self]. directory _ FileList2 modalFolderSelector ifNil: [^ self]. [self extractAllPossibleInDirectory: directory] whileFalse: [self confirm: 'Try a different directory?']. [["first extract directories if any" self extractDirectoriesIntoDirectory: directory. "then files" self extractFilesIntoDirectory: directory] on: FileStreamException do: [:ex | (self confirm: ex class name, ': ' , ex messageText , '. Continue?') ifTrue: [ex resume] ifFalse: [^ self]]] on: Error do: [ :ex | self inform: 'Error: ', ex messageText ].! ! !ArchiveViewer methodsFor: 'archive operations' stamp: 'nk 12/20/2002 14:51'! extractAllPossibleInDirectory: directory "Answer true if I can extract all the files in the given directory safely. Inform the user as to problems." | conflicts | self canExtractAll ifFalse: [ ^false ]. conflicts _ Set new. self members do: [ :ea | | fullName | fullName _ directory fullNameFor: ea localFileName. (ea usesFileNamed: fullName) ifTrue: [ conflicts add: fullName ]. ]. conflicts notEmpty ifTrue: [ | str | str _ WriteStream on: (String new: 200). str nextPutAll: 'The following file(s) are needed by archive members and cannot be overwritten:'; cr. conflicts do: [ :ea | str nextPutAll: ea ] separatedBy: [ str cr ]. self inform: str contents. ^false. ]. conflicts _ Set new. self members do: [ :ea | | fullName | fullName _ directory relativeNameFor: ea localFileName. (directory fileExists: fullName) ifTrue: [ conflicts add: fullName ]. ]. conflicts notEmpty ifTrue: [ | str | str _ WriteStream on: (String new: 200). str nextPutAll: 'The following file(s) will be overwritten:'; cr. conflicts do: [ :ea | str nextPutAll: ea ] separatedBy: [ str cr ]. str cr; nextPutAll: 'Is this OK?'. ^PopUpMenu confirm: str contents. ]. ^true. ! ! !ArchiveViewer methodsFor: 'archive operations' stamp: 'nk 11/11/2002 22:14'! extractDirectoriesIntoDirectory: directory (self members select: [:ea | ea isDirectory]) do: [:ea | ea extractInDirectory: directory]! ! !ArchiveViewer methodsFor: 'archive operations' stamp: 'nk 11/11/2002 22:13'! extractFilesIntoDirectory: directory (self members reject: [:ea | ea isDirectory]) do: [:ea | ea extractInDirectory: directory]! ! !ArchiveViewer methodsFor: 'initialization' stamp: 'nk 12/16/2002 17:12'! windowIsClosing archive close.! ! !FileDirectory methodsFor: 'file name utilities' stamp: 'nk 12/13/2002 10:07'! relativeNameFor: aFileName "Return the full name for aFileName, assuming that aFileName is a name relative to me." aFileName isEmpty ifTrue: [ ^pathName ]. ^aFileName first = self pathNameDelimiter ifTrue: [ pathName, aFileName ] ifFalse: [ pathName, self slash, aFileName ] ! ! !FileDirectory methodsFor: 'file directory' stamp: 'nk 3/13/2003 10:18'! assureExistenceOfPath: localPath "Make sure the local directory exists. If necessary, create all parts in between" localPath isEmpty ifTrue: [ ^self ]. "Assumed to exist" (self directoryExists: localPath) ifTrue: [^ self]. "exists" "otherwise check parent first and then create local dir" self containingDirectory assureExistenceOfPath: self localName. self createDirectory: localPath! ! !DosFileDirectory methodsFor: 'path access' stamp: 'nk 12/13/2002 10:05'! relativeNameFor: path "Return the full name for path, assuming that path is a name relative to me." path isEmpty ifTrue:[^pathName]. (path at: 1) = $\ ifTrue:[ (path size >= 2 and:[(path at: 2) = $\]) ifTrue:[^super relativeNameFor: path allButFirst ]. "e.g., \\pipe\" ^super relativeNameFor: path "e.g., \windows\"]. (path size >= 2 and:[(path at: 2) = $: and:[path first isLetter]]) ifTrue:[^super relativeNameFor: (path copyFrom: 3 to: path size) ]. "e.g., c:" ^pathName, self slash, path! ! !FileDirectory class methodsFor: 'platform specific' stamp: 'nk 3/13/2003 10:58'! makeAbsolute: path "Ensure that path looks like an absolute path" ^path first = self pathNameDelimiter ifTrue: [ path ] ifFalse: [ self slash, path ]! ! !FileDirectory class methodsFor: 'platform specific' stamp: 'nk 3/13/2003 10:59'! makeRelative: path "Ensure that path looks like an relative path" ^path first = self pathNameDelimiter ifTrue: [ path copyWithoutFirst ] ifFalse: [ path ]! ! !MacFileDirectory methodsFor: 'file operations' stamp: 'nk 3/13/2003 09:01'! fullPathFor: path "Return the fully-qualified path name for the given file." path isEmptyOrNil ifTrue: [^ pathName]. (self class isAbsolute: path) ifTrue: [^ path]. pathName = '' "Root dir?" ifTrue: [ ^path]. ^(path first = $:) ifTrue: [ pathName, path ] ifFalse: [pathName , ':' , path]! ! !MacFileDirectory class methodsFor: 'platform specific' stamp: 'nk 3/13/2003 10:59'! makeAbsolute: path "Ensure that path looks like an absolute path" | absolutePath | (self isAbsolute: path) ifTrue: [ ^path ]. "If a path begins with a colon, it is relative." absolutePath _ (path first = $:) ifTrue: [ path copyWithoutFirst ] ifFalse: [ path ]. (self isAbsolute: absolutePath) ifTrue: [ ^absolutePath ]. "Otherwise, if it contains a colon anywhere, it is absolute and the first component is the volume name." ^absolutePath, ':'! ! !MacFileDirectory class methodsFor: 'platform specific' stamp: 'nk 3/13/2003 10:59'! makeRelative: path "Ensure that path looks like an relative path" ^path first = $: ifTrue: [ path ] ifFalse: [ ':', path ]! ! !PositionableStream methodsFor: '*Project-SAR-fileIn' stamp: 'ab 5/23/2003 11:11'! fileInFor: client announcing: announcement "This is special for reading expressions from text that has been formatted with exclamation delimitors. The expressions are read and passed to the Compiler. Answer the result of compilation. Put up a progress report with the given announcement as the title. Does NOT handle preambles or postscripts specially." | val chunk | announcement displayProgressAt: Sensor cursorPoint from: 0 to: self size during: [:bar | [self atEnd] whileFalse: [bar value: self position. self skipSeparators. [ val _ (self peekFor: $!!) ifTrue: [ (Compiler evaluate: self nextChunk for: client logged: false) scanFrom: self ] ifFalse: [ chunk _ self nextChunk. self checkForPreamble: chunk. Compiler evaluate: chunk for: client logged: true ]. ] on: InMidstOfFileinNotification do: [ :ex | ex resume: true]. self atEnd ifFalse: [ self skipStyleChunk ]]. self close]. "Note: The main purpose of this banner is to flush the changes file." Smalltalk logChange: '----End fileIn of ' , self name , '----'. Smalltalk systemNavigation allBehaviorsDo: [ :cl | cl removeSelectorSimply: #DoIt; removeSelectorSimply: #DoItIn: ]. ^ val! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! directory ^directory! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! directory: anObject directory := anObject! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! fileName ^fileName! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! fileName: anObject fileName := anObject! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! zip ^zip! ! !SARInstaller methodsFor: 'accessing' stamp: 'nk 10/25/2002 12:16'! zip: anObject ^zip := anObject! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 11:01'! extractMember: aMemberOrName "Extract aMemberOrName to a file using its filename" self zip extractMember: aMemberOrName! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 11:01'! extractMember: aMemberOrName toFileNamed: aFileName "Extract aMemberOrName to a specified filename" self zip extractMember: aMemberOrName toFileNamed: aFileName! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 11:02'! extractMemberWithoutPath: aMemberOrName "Extract aMemberOrName to its own filename, but ignore any directory paths" self zip extractMemberWithoutPath: aMemberOrName! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 11:02'! fileInMemberNamed: csName "This is to be used from preamble/postscript code to file in zip members as ChangeSets." | cs | cs _ self zip memberNamed: csName. cs ifNil: [ self inform: 'no member named ', csName. ^self ]. self fileIntoChangeSetNamed: csName fromStream: cs contentStream ascii ! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 12/9/2002 16:12'! fileInPackageNamed: memberName "This is to be used from preamble/postscript code to file in zip members as DVS packages." | member current new baseName imagePackageLoader packageInfo streamPackageLoader | member _ self zip memberNamed: memberName. member ifNil: [self inform: 'no member named ' , memberName. ^ self]. imagePackageLoader _ Smalltalk at: #ImagePackageLoader ifAbsent: []. streamPackageLoader _ Smalltalk at: #StreamPackageLoader ifAbsent: []. packageInfo _ Smalltalk at: #PackageInfo ifAbsent: []. (packageInfo isNil or: [imagePackageLoader isNil or: [streamPackageLoader isNil]]) ifTrue: [^ self fileInMemberNamed: memberName]. baseName _ memberName copyReplaceAll: '.st' with: '' asTokens: false. current _ imagePackageLoader new package: (packageInfo named: baseName). new _ streamPackageLoader new stream: member contentStream ascii. (new changesFromBase: current) fileIn! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 10:35'! memberNamed: aString ^zip memberNamed: aString! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 10:34'! membersMatching: aString ^self zip membersMatching: aString! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 10:36'! prependedDataSize ^self zip prependedDataSize! ! !SARInstaller methodsFor: 'client services' stamp: 'nk 10/27/2002 10:35'! zipFileComment ^self zip zipFileComment! ! !SARInstaller methodsFor: 'fileIn' stamp: 'nk 10/27/2002 10:00'! fileIn "File in to a change set named like my file" | stream | stream := directory readOnlyFileNamed: fileName. self withCurrentChangeSetNamed: fileName sansPeriodSuffix do: [:cs | self fileInFrom: stream]! ! !SARInstaller methodsFor: 'fileIn' stamp: 'nk 10/25/2002 11:53'! fileInFrom: stream "The zip has been saved already by the download. Read the zip into my instvar, then file in the correct members" | preamble postscript | [ stream position: 0. zip _ ZipArchive new readFrom: stream. preamble _ zip memberNamed: 'install/preamble'. preamble ifNotNil: [ preamble contentStream ascii fileInFor: self announcing: 'Preamble'. Smalltalk changes preambleString: preamble contents. ]. postscript _ zip memberNamed: 'install/postscript'. postscript ifNotNil: [ postscript contentStream ascii fileInFor: self announcing: 'Postscript'. Smalltalk changes postscriptString: postscript contents. ]. ] ensure: [ stream close. zip _ nil. ].! ! !SARInstaller methodsFor: 'fileIn' stamp: 'nk 10/25/2002 15:06'! fileIntoChangeSetNamed: aString fromStream: stream "We let the user confirm filing into an existing ChangeSet or specify another ChangeSet name if the name derived from the filename already exists. Duplicated from SMSimpleInstaller. Should be a class-side method." self withCurrentChangeSetNamed: aString do: [:cs | | newName | newName := cs name. stream fileInAnnouncing: 'Loading ' , newName , ' into change set ''' , newName , ''''. stream close]! ! !SARInstaller methodsFor: 'private' stamp: 'nk 10/27/2002 12:46'! withCurrentChangeSetNamed: aString do: aOneArgumentBlock "Evaluate the one-argument block aOneArgumentBlock while the named change set is active. We let the user confirm operating on an existing ChangeSet or specify another ChangeSet name if the name derived from the filename already exists. Duplicated from SMSimpleInstaller. Should be a class-side method. Returns change set." | changeSet newName oldChanges | newName := aString. changeSet := self class changeSetNamed: newName. changeSet ifNotNil: [newName := FillInTheBlank request: 'ChangeSet already present, just confirm to overwrite or enter a new name:' initialAnswer: newName. newName isEmpty ifTrue: [self error: 'Cancelled by user']. changeSet := self class changeSetNamed: newName]. changeSet ifNil: [changeSet := self class basicNewChangeSet: newName]. changeSet ifNil: [self error: 'User did not specify a valid ChangeSet name']. oldChanges := Smalltalk changes. [Smalltalk newChanges: changeSet. aOneArgumentBlock value: changeSet] ensure: [Smalltalk newChanges: oldChanges]. ^changeSet! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/13/2002 07:33'! fileReaderServicesForFile: fullName suffix: suffix ^(suffix = 'sar') | (suffix = '*') ifTrue: [Array with: self serviceFileInSAR] ifFalse: [#()] ! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/13/2002 07:52'! initialize "SARInstaller initialize" (FileList respondsTo: #registerFileReader:) ifTrue: [ FileList registerFileReader: self ]! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/13/2002 07:45'! installSAR: fullName FileDirectory splitName: fullName to: [ :dir :fileName | (self directory: (FileDirectory on: dir) fileName: fileName) fileIn ]! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/13/2002 07:35'! serviceFileInSAR "Answer a service for opening a changelist browser on a file" ^ SimpleServiceEntry provider: self label: 'install SAR' selector: #installSAR: description: 'install this Squeak ARchive into the image.' buttonLabel: 'install'! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/21/2002 09:46'! services ^Array with: self serviceFileInSAR ! ! !SARInstaller class methodsFor: 'class initialization' stamp: 'nk 11/13/2002 07:53'! unload (FileList respondsTo: #unregisterFileReader:) ifTrue: [ FileList unregisterFileReader: self ]! ! !SARInstaller class methodsFor: 'change set utilities' stamp: 'nk 10/27/2002 12:44'! basicNewChangeSet: newName Smalltalk at: #ChangeSorter ifPresentAndInMemory: [ :cs | ^cs basicNewChangeSet: newName ]. (self changeSetNamed: newName) ifNotNil: [ self inform: 'Sorry that name is already used'. ^nil ]. ^ChangeSet basicNewNamed: newName.! ! !SARInstaller class methodsFor: 'change set utilities' stamp: 'nk 10/27/2002 12:44'! changeSetNamed: newName Smalltalk at: #ChangeSorter ifPresentAndInMemory: [ :cs | ^cs changeSetNamed: newName ]. ^ChangeSet allInstances detect: [ :cs | cs name = newName ] ifNone: [ nil ].! ! !SARInstaller class methodsFor: 'instance creation' stamp: 'nk 10/27/2002 10:29'! directory: dir fileName: fn ^(self new) directory: dir; fileName: fn; yourself.! ! !ZipArchive methodsFor: 'archive operations' stamp: 'nk 12/16/2002 17:09'! readFrom: aStreamOrFileName | stream name eocdPosition | stream _ aStreamOrFileName isStream ifTrue: [name _ aStreamOrFileName name. aStreamOrFileName] ifFalse: [StandardFileStream readOnlyFileNamed: (name _ aStreamOrFileName)]. stream binary. eocdPosition _ self class findEndOfCentralDirectoryFrom: stream. eocdPosition <= 0 ifTrue: [self error: 'can''t find EOCD position']. self readEndOfCentralDirectoryFrom: stream. stream position: eocdPosition - centralDirectorySize. self readMembersFrom: stream named: name! ! !ZipArchiveMember methodsFor: 'accessing' stamp: 'nk 12/20/2002 14:49'! extractInDirectory: dir self extractToFileNamed: self localFileName inDirectory: dir ! ! !ZipArchiveMember methodsFor: 'accessing' stamp: 'nk 11/11/2002 14:08'! extractToFileNamed: aFileName self extractToFileNamed: aFileName inDirectory: FileDirectory default.! ! !ZipArchiveMember methodsFor: 'accessing' stamp: 'nk 2/26/2003 13:06'! extractToFileNamed: aLocalFileName inDirectory: dir | stream fullName fullDir | self isEncrypted ifTrue: [ ^self error: 'encryption unsupported' ]. fullName _ dir fullNameFor: aLocalFileName. fullDir _ FileDirectory forFileName: fullName. fullDir assureExistence. self isDirectory ifFalse: [ stream _ fullDir forceNewFileNamed: (FileDirectory localNameFor: fullName). self extractTo: stream. stream close. ] ifTrue: [ fullDir assureExistence ] ! ! !ZipArchiveMember methodsFor: 'accessing' stamp: 'nk 3/13/2003 09:23'! localFileName "Answer my fileName in terms of the local directory naming convention" | localName | localName _ fileName copyReplaceAll: '/' with: FileDirectory slash. ^(fileName first = $/) ifTrue: [ FileDirectory default class makeAbsolute: localName ] ifFalse: [ FileDirectory default class makeRelative: localName ]! ! !ZipArchiveMember methodsFor: 'accessing' stamp: 'nk 11/11/2002 21:03'! splitFileName "Answer my name split on slash boundaries. A directory will have a trailing empty string." ^ fileName findTokens: '/'.! ! !ZipFileMember methodsFor: 'private-reading' stamp: 'nk 11/11/2002 21:46'! canonicalizeFileName "For security reasons, make all paths relative and remove any ../ portions" [fileName beginsWith: '/'] whileTrue: [fileName := fileName allButFirst]. fileName := fileName copyReplaceAll: '../' with: ''! ! !ZipFileMember methodsFor: 'private-reading' stamp: 'nk 11/11/2002 21:48'! readFrom: aStream "assumes aStream positioned after CD header; leaves stream positioned after my CD entry" self readCentralDirectoryFileHeaderFrom: aStream. self readLocalDirectoryFileHeaderFrom: aStream. self endRead. self canonicalizeFileName. ! ! !ZipDirectoryMember methodsFor: 'accessing' stamp: 'nk 12/20/2002 14:45'! localFileName: aString | dir entry parent | super localFileName: aString. fileName last = $/ ifFalse: [ fileName _ fileName, '/' ]. parent _ FileDirectory default. (parent directoryExists: fileName) ifTrue: [ dir _ FileDirectory on: (parent fullNameFor: fileName). entry _ dir directoryEntry. self setLastModFileDateTimeFrom: entry modificationTime ] ! ! !ZipDirectoryMember class methodsFor: 'as yet unclassified' stamp: 'nk 12/20/2002 14:57'! newNamed: aFileName ^(self new) localFileName: aFileName; yourself! ! !ZipNewFileMember methodsFor: 'initialization' stamp: 'nk 12/20/2002 15:01'! from: aFileName | entry | compressionMethod _ CompressionStored. "Now get the size, attributes, and timestamps, and see if the file exists" stream _ StandardFileStream readOnlyFileNamed: aFileName. self localFileName: (externalFileName _ stream name). entry _ stream directoryEntry. compressedSize _ uncompressedSize _ entry fileSize. desiredCompressionMethod _ compressedSize > 0 ifTrue: [ CompressionDeflated ] ifFalse: [ CompressionStored ]. self setLastModFileDateTimeFrom: entry modificationTime ! ! !ZipStringMember class methodsFor: 'as yet unclassified' stamp: 'nk 12/20/2002 15:06'! newFrom: aString named: aFileName ^(self new) contents: aString; localFileName: aFileName; yourself! ! SARInstaller initialize! "Postscript:" "Register package 'SARInstaller for 3.4' 15 as installed" SMSqueakMap default noteInstalledPackage: '16dff307-ff49-4996-a216-957989e92d48' version: '15'. !