Wiki

Ticket #209: InstallFromWorkspace.cobra

File InstallFromWorkspace.cobra, 21.2 KB (added by nevdelap, 14 years ago)
Line 
1"""
2InstallFromWorkspace.cobra
3
4This program installs Cobra onto your system out of the workspace. It's a great
5convenience for users who only grab the workspace to get the very latest
6version of Cobra (e.g., they are not developing on the compiler). But even for
7developers, it's convenient to "snapshot" the compiler for your system when
8desired.
9
10Normally the compiler and libraries to be installed are compiled with -turbo.
11However, you can pass an argument to this program such as -debug which will
12replace -turbo. This is useful for troubleshooting.
13
14Why not a .bat file? Won't run on Mac, Linux, etc.
15
16Why not a bash script? Won't run on Windows-sans-cygwin.
17
18Also, as a .NET program this installer has easy access to the GAC and any other
19.NET resources. Furthermore, as a Cobra programmer, any Cobra user could
20potentially hack on it.
21
22
23TO RUN
24
25    On Windows
26
27        Update, if desired:
28        > cd \path\to\Workspace
29        > svn up
30
31        Get into the Source:
32        > cd Source
33
34        Use the batch file:
35        > install-from-workspace
36
37    On Mac, Linux, etc.
38
39        Update, if desired:
40        $ cd /path/to/Workspace
41        $ svn up
42
43        Get into the Source:
44        $ cd Source
45
46        Use the script:
47        $ sudo ./install-from-workspace
48
49
50    The installer prints "Success!" at the end, if successful.
51    Otherwise you will see an error message.
52
53    It's safe to run the installer more than once.
54
55    The installer does not change any workspace files that are under source code control.
56
57    To test the installation, try invoking Cobra from *outside* the workspace.
58
59
60ASSUMPTIONS
61
62    * Your system meets the requirements for Novell Mono 2.0 or .NET 2.0
63      http://msdn.microsoft.com/en-us/library/ms229070.aspx
64
65    * This program is run in the Workspace\Source directory.
66
67    * This program is run by Snapshot\cobra.exe. *NOT* .\cobra.exe
68
69    * The current Cobra compiler in Source\ will report a -version equal to the
70      last released version plus optional additional text. If the last release
71      was "0.8.0" then the new release should be "0.8.0 post".
72
73    * The current Cobra compiler in Source\ will have three components to its
74      base version *number*. Form: x.y.z  Example: 0.8.0
75
76
77TODO
78
79    [ ] Create an install log
80
81    [ ] Various "TODO" items in the source itself
82
83"""
84
85use System.Diagnostics
86use System.Reflection
87use System.Text.RegularExpressions
88
89# for GAC installation
90use System.EnterpriseServices
91use System.EnterpriseServices.Internal
92
93
94class InstallFromWorkspace
95
96    var installDirectories = [r'C:\Cobra', '/usr/local/cobra']
97
98    var configFileName = 'install-directory.text'
99
100    def main is shared
101        InstallFromWorkspace().run
102
103    var _lastCommand as String?
104
105    var _snapshotCompilerPath as String?
106    var _gacutil as String?
107    var _baseVersion as String?       # ex: '0.8.0-post', '0.8.0-post-2009-03-01'
108    var _targetDir as String?         # ex: 'C:\Cobra', '/usr/local/cobra'
109    var _versionDir as String?        # ex: 'C:\Cobra\Cobra-0.8.0-post', 'C:\Cobra\Cobra-0.8.0-post-2009-03-01', '/usr/local/cobra/Cobra-0.8.1-post'
110    var _cobraCommandPath as String?  # ex: 'C:\Cobra\bin\cobra.bat', '/usr/local/cobra/bin/cobra'
111
112
113    def run
114        print
115        print 'The Cobra Programming Language'
116        print 'Install From Workspace'
117        print
118        print 'Working...'
119        print
120        .verifyInstallDirectory
121        .verifyInWorkspace
122        .verifyVirtualMachineVersion
123        .locateSnapshotCompiler
124        # .locateGacUtil
125        .buildCobraCompiler
126        .getBaseVersion
127        .buildCobraLibrary
128        .verifyNewlyBuiltCobra
129        .installCobraLibraryToGAC
130        .copyFilesToInstallDirectory
131        .verifyNewlyInstalledCobra
132        .cleanUpWorkspace
133        .installInPath
134        print
135        print 'Success!'
136
137
138    ## Self utils
139
140    get compileFlags as String
141        args = CobraCore.commandLineArgs
142        if args.count > 1, return args[1]  # TODO: should be a join of args[1:]
143        else, return '-turbo'
144
145    get isRunningOnUnix as bool
146        """
147        Returns true if the current process is running on Unix/Posix/Linux/BSD/etc.
148        """
149        platform = Environment.osVersion.platform to int
150        return platform in [4, 128] # http://www.mono-project.com/FAQ:_Technical
151
152    get lastCommand from var
153
154    get slash as char
155        return Path.directorySeparatorChar
156
157    def error(msg)
158        print '** ERROR:', msg
159        Environment.exit(1)
160        print 'Exiting from error.'
161
162    def warning(msg)
163        print '** WARING:', msg
164
165
166    ## File system
167
168    def copyContents(source as String, target as String)
169        print 'copy from:', source
170        print '       to:', target
171        .copyContents(DirectoryInfo(source), DirectoryInfo(target))
172
173    def copyContents(source as DirectoryInfo, target as DirectoryInfo)
174        if not target.exists, target.create
175        for sourceFile in source.getFiles
176            sourceFile.copyTo(Path.combine(target.fullName, sourceFile.name), true)
177        for sourceSubDir in source.getDirectories
178            targetSubDir = target.createSubdirectory(sourceSubDir.name)
179            .copyContents(sourceSubDir, targetSubDir)
180
181    def deleteDir(dir as String)
182        if Directory.exists(dir)
183            print 'del dir  :', dir
184            spacer = '          '
185            _unReadOnly(dir)
186            numAttempts = 3
187            for attempt in 1 : numAttempts + 1
188                try
189                    Directory.delete(dir, true# true = recursive
190                catch IOException
191                    # sometimes "The directory is not empty." occurs
192                    if attempt == numAttempts, throw
193                    print spacer, 'Having trouble deleting directory. Try again in [attempt] seconds.'
194                    System.Threading.Thread.sleep(attempt*1_000)
195                    if not Directory.exists(dir# and sometimes it goes away!
196                        print spacer, 'Directory is gone.'
197                if not Directory.exists(dir), break
198
199    def _unReadOnly(dirName as String)
200        _unReadOnly(DirectoryInfo(dirName))
201
202    def _unReadOnly(dir as DirectoryInfo)
203        # print 'checking -', dir
204        for file in dir.getFiles
205            if sharp'file.Attributes & System.IO.FileAttributes.ReadOnly'
206                # print 'changing -', file
207                file.attributes = sharp'file.Attributes & ~System.IO.FileAttributes.ReadOnly'
208                # file.attributes = file.attributes & ~FileAttributes.ReadOnly
209        for subDir in dir.getDirectories
210            _unReadOnly(subDir)
211
212    def findAndDeleteDir(baseDir as String, findDir as String)
213        .findAndDeleteDir(DirectoryInfo(baseDir), findDir)
214
215    def findAndDeleteDir(baseDir as DirectoryInfo, findDir as String)
216        for sourceSubDir in baseDir.getDirectories
217            if sourceSubDir.name == findDir
218                .deleteDir(sourceSubDir.fullName)
219            else
220                .findAndDeleteDir(sourceSubDir, findDir)
221
222    def requireDir(dir as String)
223        if Directory.exists(dir)
224            print 'found dir:', dir
225        else
226            print 'make dir :', dir
227            try
228                Directory.createDirectory(dir)
229            catch ex as SystemException
230                .error('Unable to create installation directory.\n[ex.message]\nRun as admin, or put a correct install path into a file called "[.configFileName]".')
231
232    def startStage(description as String)
233        print '====', description
234        print
235        # this installer relies on there being no directory changes
236        assert Environment.currentDirectory.endsWith('Source')
237
238
239    ## Running external commands
240
241    def runCommand(command as String, args as String) as String
242        return .runCommand(command, args, true)
243
244    def runCommand(command as String, args as String, displayOutput as bool) as String
245        process as Process?
246        output = .runCommand(command, args, out process, displayOutput)
247        if process.exitCode, .error('Exit code from above command: [process.exitCode]')
248        return output
249
250    def runCommand(command as String, args as String, process as out Process?) as String
251        return .runCommand(command, args, out process, true)
252
253    def runCommand(command as String, args as String, process as out Process?, displayOutput as bool) as String
254        """
255        Runs the given external command with the given args.
256        Sets the process to the instance of Process created for this purpose.
257        Returns the output, which is also displayed if displayOutput is true.
258        Does not check process.exitCode.
259        """
260        print 'run: [command] [args]'
261        _lastCommand = command + ' ' + args
262        p = Process()
263        p.startInfo.fileName = command
264        p.startInfo.arguments = args
265        output = CobraCore.runAndCaptureAllOutput(p).trim
266        process = p
267        if displayOutput and output <> ''
268            for line in output.replace('\r', '').split(c'\n')
269                print '   : [line]'
270        print
271        return output
272
273    def runSnapshotCobra(args as String) as String
274        """
275        Runs the Cobra compiler.
276        Prints the output and returns it.
277        If Cobra gives an error, calls .error.
278        """
279        process as Process?
280        output = .runSnapshotCobra(args, out process)
281        if process.exitCode, .error('Exit code from running SnapshotCobra: [process.exitCode]')
282        return output
283
284    def runSnapshotCobra(args as String, process as out Process?) as String
285        """
286        Runs the Cobra compiler.
287        Creates and "returns" the Process instance via the `process` argument.
288        Prints the output and returns it.
289        This does not exit on any errors. Check the process yourself.
290        """
291        return .runCommand(_snapshotCompilerPath to !, args, out process)
292
293    def runSourceCobra(args as String) as String
294        process as Process?
295        output = .runSourceCobra(args, out process)
296        if process.exitCode, .error('Exit code from running Source Cobra: [process.exitCode]')
297        return output
298
299    def runSourceCobra(args as String, process as out Process?) as String
300        return .runCommand('cobra.exe', args, out process)
301
302
303    ## Stages
304
305    def verifyInstallDirectory
306        .startStage('Verify installation directory')
307        default = if(.isRunningOnUnix, .installDirectories[1], .installDirectories[0])
308        if File.exists(.configFileName)
309            # get install directory from first line of the file
310            useDir = File.openText(.configFileName).readLine
311            if useDir and useDir.trim.length, default = useDir.trim
312        # TODO: prompt the user for the location
313        .requireDir(default)
314        _targetDir = default
315        print
316
317    def verifyInWorkspace
318        .startStage('Verify running in workspace')
319        msg = 'The current directory does not appear to be a workspace. This program is for installing from the workspace.'
320        if not File.exists('Compiler.cobra'), .error(msg)
321        if not Directory.exists('Snapshot'), .error(msg)
322        print 'Verified.'
323        print
324
325    def verifyVirtualMachineVersion
326        if CobraCore.isRunningOnMono
327            .startStage('Verify Novell Mono version')
328            if CobraCore.isRunningOnMono
329                minMonoVersion = '2.0'
330                vers = CobraCore.monoVersionString
331                if vers is nil or vers.trim == ''
332                    .error('Cannot determine Mono version. Please install Mono [minMonoVersion] or higher.')
333                else
334                    print 'Mono Version', vers
335                    parts = vers.split(c'.')
336                    if parts and parts[0] >= '2'
337                        print 'Verified >= [minMonoVersion]'
338                        print
339                    else
340                        .error('Mono version must be [minMonoVersion] or higher.')
341
342    def locateSnapshotCompiler
343        .startStage('Locate and test Cobra Snapshot compiler')
344        _snapshotCompilerPath = 'Snapshot[.slash]cobra.exe'
345        if not File.exists(_snapshotCompilerPath)
346            .error('Cannot locate [_snapshotCompilerPath].')
347        # Not needed; clutters the output; tends to obscure when the Snapshot is being used;
348        # _snapshotCompilerPath = Path.getFullPath(_snapshotCompilerPath)
349        output = .runSnapshotCobra('-about')
350        assert 'The Cobra Programming Language' in output
351        assert 'Copyright' in output
352        assert 'Usage' in output
353
354    def locateGacUtil(startStage as bool)
355        if startStage, .startStage('Locate gacutil.exe')
356        slash = .slash
357        gacutil = 'gacutil'
358        p = Process()
359        p.startInfo.fileName = gacutil
360        p.startInfo.arguments = '-silent'
361        try
362            CobraCore.runAndCaptureAllOutput(p)
363            found = true
364            print 'found in system PATH'
365        catch FileNotFoundException
366            pass
367        catch ComponentModel.Win32Exception
368            pass
369
370        if not found
371            # try to find the gacutil
372            print 'Searching for gacutil...'
373            gacutil = 'gacutil.exe'
374
375            dirs = [
376                r'Microsoft.NET\SDK\v2.0\bin',        # VS 2005 / SDK .NET 2.0
377                r'Microsoft SDKs\Windows\v6.0A\bin',  # VS 2008
378                r'Microsoft Visual Studio 8\SDK\v2.0\Bin',
379            ]
380
381            # search %ProgramFiles% (which can differ internationally)
382            programFilesDir = Environment.getFolderPath(Environment.SpecialFolder.ProgramFiles)
383            for dir in dirs
384                path = '[programFilesDir][slash][dir][slash][gacutil]'
385                print 'checking:', path
386                if File.exists(path)
387                    print 'found:', path
388                    gacutil = path
389                    found = true
390                    break
391                # else, print path
392
393            if not found
394                for dir in dirs
395                    # search drives for X:\Program Files\...
396                    for drive in Directory.getLogicalDrives
397                        drive = drive[:1]  # normalize to just one character
398                        if drive <= 'B', continue
399                        path = '[drive]:[slash]Program Files[slash][dir][slash][gacutil]'
400                        print 'checking:', path
401                        if File.exists(path)
402                            print 'found:', path
403                            gacutil = path
404                            found = true
405                            break
406                        # else, print path
407                    if found, break
408
409        if not found
410            .error('Cannot locate a gacutil. Maybe you can try again using the "Visual Studio Command Prompt" or ".NET SDK Command Prompt".')
411
412        _gacutil = gacutil
413        print
414
415    def buildCobraCompiler
416        .startStage('Build new Cobra compiler')
417        .runSnapshotCobra('-compile [.compileFlags] -ert:yes cobra.cobra -files:files-to-compile.text')
418
419    def getBaseVersion
420        .startStage('Retrieve Cobra base version number')
421        # It's called "base version" because it doesn't include any text after the numbers
422        # "0.8.0 post release" --> "0.8.0"
423        output = .runSourceCobra('-version')
424        re = Regex(r'\d+\.\d+\.\d+')
425        reMatch = re.match(output)
426        if reMatch.success
427            assert reMatch.value
428            _baseVersion = reMatch.value
429        else
430            .error('Could not extract base version number.')
431        _baseVersion += '-post'
432        # check for an informal release
433        parent = Path.getFullPath('..')
434        if File.exists('[parent][.slash]InformalRelease.text')
435            output = File.readAllText('[parent][.slash]InformalRelease.text')
436            re = Regex(r'\d+\-\d+\-\d+')
437            reMatch = re.match(output)
438            if reMatch.success
439                assert reMatch.value
440                _baseVersion += '-' + reMatch.value
441            else
442                .error('Could not extract date from InformalRelease.text')
443        print 'base version: [_baseVersion]'
444        print
445
446    def buildCobraLibrary
447        .startStage('Build Cobra standard library')
448        # must build the library using the Source cobra.exe, not the Snapshot
449        .runSourceCobra('-bsl [.compileFlags] Cobra.Lang[.slash]AssemblyAttrs.cobra -key-file:Cobra.Lang[.slash]Cobra.Lang.snk')
450
451    def verifyNewlyBuiltCobra
452        .startStage('Verify newly built Cobra compiler')
453        .runSourceCobra('-about')
454        msg = 'Cannot run hello.cobra with new compiler.'
455        output = .runSourceCobra('-ert:no hello')
456        if not output.startsWith('Hello'), .error(msg)
457        output = .runSourceCobra('-ert:yes hello')
458        if not output.startsWith('Hello'), .error(msg)
459        output = .runSourceCobra('hello')
460        if not output.startsWith('Hello'), .error(msg)
461
462    def installCobraLibraryToGAC
463        .startStage('Install Cobra standard library to the GAC')
464        print 'Invoking Publish.GacInstall...'
465        try
466            Publish().gacInstall('Cobra.Lang.dll')
467            print 'Done.'
468        catch NotImplementedException
469            print 'Not implemented exception.'
470            print 'Will attempt to use gacutil.exe.'
471            .locateGacUtil(false)
472            .runCommand(_gacutil, '-i Cobra.Lang.dll -f')
473            output = .runCommand(_gacutil, '-l Cobra.Lang')
474            if 'Cobra.Lang, Version=' not in output
475                .error('Cannot see Cobra.Lang in the gacutil output to list it above.')
476            # TODO: verify the version number: Cobra.Lang, Version=0.0.1.0
477        print
478
479    def copyFilesToInstallDirectory
480        require _targetDir is not nil
481        .startStage('Copy files to install directory')
482        slash = .slash
483        targetDir = _targetDir
484
485        if targetDir, .requireDir(targetDir)
486        if Directory.exists(Path.combine(targetDir, 'Source'))
487            print 'The directory "[targetDir]" appears to contain a workspace or snapshot of the'
488            print 'Cobra source code, due to containing the subdirectory "Source".'
489            print 'Installing to that location can lead to confusion and technical difficulties.'
490            print 'Consider clearing out "[targetDir]" and creating a workspace *inside* it called,'
491            print 'for example, "Workspace".'
492            print
493            .error('Cannot install to workspace or workspace snapshot. See message above.')
494
495        versionDir = '[targetDir][slash]Cobra-[_baseVersion]'
496        .deleteDir(versionDir)
497        .requireDir(versionDir)
498        _versionDir = versionDir
499
500        # TODO: readme file?
501        parent = Path.getFullPath('..')
502
503        versionOutput = ''
504        if File.exists('[parent][slash]InformalRelease.text')
505            versionOutput = File.readAllText('[parent][slash]InformalRelease.text')
506        else
507            # record "svn info" in the installation directory
508            svnFailed = 'Cannot find version information, svn command line version not installed or failed.'
509            try
510                process as Process?
511                versionOutput = .runCommand('svn', 'info', out process, false)
512                if process.exitCode
513                    print '"svn info" failed. Check your svn installation.'
514                    print '[versionOutput]'
515                    print
516                    .warning(svnFailed)
517            catch ex as SystemException
518                # user could be on TortoiseSVN
519                print 'svn is not installed or it is not in the PATH. ', ex.message
520                .warning(svnFailed)
521        if versionOutput <> ''
522            fileName = '[versionDir][slash]Version.text'
523            print 'writing  :', fileName
524            File.writeAllText(fileName, versionOutput)
525        print
526
527        for dir in ['HowTo', 'Reusables', 'Samples', 'Supplements']
528            .copyContents('[parent][slash][dir]', '[versionDir][slash][dir]')
529
530        versionBinDir = '[versionDir][slash]bin'
531        .requireDir(versionBinDir)
532        print 'copy  bin: [versionBinDir]'
533        binFiles = 'cobra.exe Cobra.Lang.dll Cobra.Sharp.dll styles-cobra-doc.css styles-exception-report.css styles-output-html.css'.split
534        for fileName in binFiles
535            File.copy(fileName, '[versionBinDir][slash][fileName]')
536
537        .copyContents('Cobra.Lang', '[versionBinDir][slash]Cobra.Lang')
538
539        # delete _svn or .svn from the installation directory
540        .findAndDeleteDir(versionDir, '_svn')
541        .findAndDeleteDir(versionDir, '.svn')
542
543        # create cobra.bat / cobra for invoking the latest version
544        baseBinDir = '[targetDir][slash]bin'
545        .requireDir(baseBinDir)
546        if .isRunningOnUnix
547            _cobraCommandPath = '[baseBinDir][slash]cobra'
548            print 'writing  :', _cobraCommandPath
549            using f = File.createText(_cobraCommandPath)
550                f.writeLine('#!/bin/sh')
551                f.writeLine('exec mono "[versionDir][slash]bin[slash]cobra.exe" "$@"')
552            .runCommand('chmod', "a+x '[_cobraCommandPath]'")
553        else
554            _cobraCommandPath = '[baseBinDir][slash]cobra.bat'
555            print 'writing  :', _cobraCommandPath
556            using f = File.createText(_cobraCommandPath)
557                f.writeLine('@"[versionDir][slash]bin[slash]cobra.exe" %*')
558            print
559
560    def verifyNewlyInstalledCobra
561        .startStage('Verify newly installed Cobra compiler')
562        output = .runCommand(_cobraCommandPath, '-about', true)
563        if not output.startsWith('The Cobra Programming Language')
564            .error('Cannot run the installed Cobra with -about')
565        output = .runCommand(_cobraCommandPath, '-ert:yes hello', true)
566        if not output.startsWith('Hello')
567            .error('Cannot run the installed Cobra on "hello.cobra"')
568
569    def cleanUpWorkspace
570        .startStage('Clean up workspace')
571        .runSnapshotCobra('-compile -debug -ert:yes cobra.cobra -files:files-to-compile.text')
572        .runSourceCobra('-bsl -debug')
573
574    def installInPath
575        .startStage('Install "cobra" into a system path directory')
576        commandName = if(.isRunningOnUnix, 'cobra', 'cobra.bat')
577        slash = .slash
578        paths = (Environment.getEnvironmentVariable('PATH') ? '').split(Path.pathSeparator)
579        for commandDir in paths
580            if commandDir == '.', continue  # skip the 'cobra' in the <workspace>/Source directory
581            # print 'checking path:', commandDir
582            if commandDir.startsWith('~')
583                home = Environment.getEnvironmentVariable('HOME')
584                if home, commandDir = home + commandDir[1:]
585            commandPath = Path.getFullPath('[commandDir][slash][commandName]')
586            if File.exists(commandPath)
587                print 'found in PATH:', commandPath
588                found = true
589                break
590        print
591        baseBinDir = '[_targetDir][slash]bin'
592        if found
593            if String.compare(commandPath, '[baseBinDir][slash][commandName]', not .isRunningOnUnix) == 0
594                print 'Your PATH already contains [baseBinDir]'
595                print 'so you can invoke "cobra" from any directory.'
596            else
597                if .newlyCompiledFileIsSymlinkedFromOneFoundOnPath(_cobraCommandPath, commandPath)
598                    print 'The existing "cobra" in your path is a symbolic'
599                    print 'link and has not been changed.'
600                else
601                    print 'copy from:', _cobraCommandPath
602                    print '       to:', commandPath
603                    File.copy(_cobraCommandPath, commandPath, true)
604                    print
605                    print 'The existing "cobra" in your path has been replaced'
606                    print 'with the new one.'
607        else
608            # TODO: on unix, create a symlink in /usr/local/bin or /usr/bin if they exist and are in the path
609            print 'The Cobra compiler is not in your PATH. To remedy you can:'
610            print '1. Add [Path.getDirectoryName(_cobraCommandPath)] to your PATH'
611            print '2. Copy [_cobraCommandPath] to any directory in your PATH'
612
613    def newlyCompiledFileIsSymlinkedFromOneFoundOnPath(cobraCommandPath, commandPath) as bool
614        if .isRunningOnUnix
615            monoAssembly = Assembly.load('Mono.Posix, Version=2.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756')
616            unixFileSystemInfo as Type = monoAssembly.getType('Mono.Unix.UnixFileSystemInfo')
617            fileInfo as dynamic = .getFileSystemEntry(unixFileSystemInfo, commandPath) # When ticket 208 is fixed delete this
618            #fileInfo as dynamic = unixFileSystemInfo.getFileSystemEntry(commandPath)  # and uncomment this.
619            if fileInfo.isSymbolicLink
620                unixSymbolicLinkInfo as Type = monoAssembly.getType('Mono.Unix.UnixSymbolicLinkInfo')
621                symbolicLinkInfo as dynamic = Convert.changeType(fileInfo, unixSymbolicLinkInfo) to !
622                if symbolicLinkInfo.contentsPath == _cobraCommandPath
623                    return true
624        return false
625
626    # When ticket 208 is fixed delete this.
627    def getFileSystemEntry(unixFileSystemInfo as Type, commandPath as String) as dynamic
628        return unixFileSystemInfo.invokeMember('GetFileSystemEntry', _
629                                               BindingFlags(Static, InvokeMethod), _
630                                               nil, _
631                                               nil, _
632                                               @[commandPath]) to !
633