]> git.sur5r.net Git - cc65/commitdiff
Simple windows installer/uninstaller
authorcuz <cuz@b7a2c559-68d2-44c3-8de9-860c34a00d81>
Fri, 25 Mar 2005 21:34:00 +0000 (21:34 +0000)
committercuz <cuz@b7a2c559-68d2-44c3-8de9-860c34a00d81>
Fri, 25 Mar 2005 21:34:00 +0000 (21:34 +0000)
git-svn-id: svn://svn.cc65.org/cc65/trunk@3416 b7a2c559-68d2-44c3-8de9-860c34a00d81

packages/windows/install.vbs [new file with mode: 0644]
packages/windows/make-cc65.bat [deleted file]
packages/windows/uninstall.vbs [new file with mode: 0644]
packages/windows/wm_envchange.c [new file with mode: 0644]

diff --git a/packages/windows/install.vbs b/packages/windows/install.vbs
new file mode 100644 (file)
index 0000000..e8c601f
--- /dev/null
@@ -0,0 +1,1148 @@
+Option Explicit                 ' Variables must be declared explicitly
+
+
+
+'******************************************************************************
+' Installer defaults.
+'******************************************************************************
+const AppName     = "cc65"
+const Version     = "2.10.1"
+const Installer   = "cc65 Installer"
+const SpaceNeeded = 20                  ' Free space needed on drive in MB
+const Shortcut    = true                ' Create shortcut on desktop
+
+
+
+'******************************************************************************
+' Global constants
+'******************************************************************************
+const SysEnv  = "HKLM\System\CurrentControlSet\Control\Session Manager\Environment"
+const SysPath = "HKLM\System\CurrentControlSet\Control\Session Manager\Environment\Path"
+
+
+
+'******************************************************************************
+' Global variables
+'******************************************************************************
+dim Tab, NewLine                ' String constants
+dim Shell, FSO                  ' Global objects
+dim ProgArgs                    ' Program arguments
+dim Dbg                         ' Output debugging stuff
+dim Language                    ' Program language
+dim SystemDrive                 ' The system drive
+dim SystemRoot                  ' The windows directory
+dim UserName                    ' Name if the current user
+dim UserProfile                 ' User profile directory
+dim ProgramFiles                ' Program files directory
+dim AppData                     ' Application data directory
+dim InstallSource               ' Installation source directory
+dim InstallTarget               ' Installation target directory
+dim UninstallCtrlFileName       ' Name of the control file for the uninstaller
+dim UninstallCtrlFile           ' Control file for the uninstaller
+dim Uninstaller                 ' Path to the uninstaller file
+dim UninstallerCmdLine          ' Command line for the uninstaller
+dim Programs                    ' "Programs" menu folder
+dim Desktop                     ' "Desktop" menu folder
+dim RegUninstall                ' Registry key for uninstall entries
+dim BinDir                     ' Directory for binaries
+dim LibDir                     ' Library directory
+dim IncDir                     ' Include directory
+dim DocIndexFile               ' Name of documentation index file
+dim AnnouncementFile           ' Name of the announcement file
+
+
+
+'******************************************************************************
+' Display an error message window with an OK button
+'******************************************************************************
+sub ErrorMsg (Msg)
+    call MsgBox (Msg, vbOkOnly + vbExclamation, Installer)
+end sub
+
+
+
+'******************************************************************************
+' Display an error message window and abort the installer
+'******************************************************************************
+sub Abort (Msg)
+    call ErrorMsg (Msg)
+    WScript.Quit (1)
+end sub
+
+
+
+'******************************************************************************
+' Display a message with an OK button
+'******************************************************************************
+sub Message (Msg)
+    call MsgBox (Msg, vbOkOnly + vbInformation, Installer)
+end sub
+
+
+
+'******************************************************************************
+' Convert a number to a string
+'******************************************************************************
+function ToString (Num)
+    ToString = FormatNumber (Num, vbFalse, vbTrue, vbFalse, vbFalse)
+end function
+
+
+
+'******************************************************************************
+' Return a message in the current language
+'******************************************************************************
+function GetMsg (Key)
+    dim Msg
+
+    ' Handle other languages here
+
+    ' Default is english
+    if IsEmpty (Msg) then
+        ' No assignment, use english
+        select case Key
+            case "MSG_ABORTINFO"
+                Msg = "Installation was aborted."
+            case "MSG_ADMIN"
+                Msg = "You must be Administrator to install %1."
+                Msg = Msg & " Are you sure you want to continue?"
+            case "MSG_COPYERR"
+                Msg = "Cannot copy %1 to %2: " & NewLine & "%3"
+            case "MSG_CREATEDIR"
+                Msg = "%1 does not exist." & NewLine & "Create it?"
+            case "MSG_CREATEERR"
+                Msg = "Cannot create %1:" & NewLine & "%2"
+            case "MSG_DELETEERR"
+                Msg = "Cannot delete %1:" & NewLine & "%2"
+            case "MSG_DRIVESPACE"
+                Msg = "Not enough space left on drive %1" & NewLine
+                Msg = Msg & "At least %2 MB are needed."
+            case "MSG_INSTALLPATH"
+                Msg = "The package will be installed in %1"
+            case "MSG_DOCENTRY"
+                Msg = "%1 Documentation"
+            case "MSG_REGREADERR"
+                Msg = "Installation failed: Cannot read the registry!"
+            case "MSG_REGWRITEERR"
+                Msg = "Installation failed: Cannot write to the registry!"
+            case "MSG_REMOVEENTRY"
+                Msg = "Remove %1"
+            case "MSG_REMOVEDIR"
+                Msg = "A folder with the name %1 does already exist."
+                Msg = Msg & " Is it ok to remove the folder?"
+            case "MSG_REMOVEOLD"
+                Msg = "Found an old version. Remove it?"
+            case "MSG_SUCCESS"
+                Msg = "Installation was successful!"
+            case "MSG_UNINSTALLERR"
+                Msg = "There was a problem uninstalling the old version. Please"
+                Msg = Msg & " uninstall the old program manually and restart"
+                Msg = Msg & " the installation."
+            case "MSG_ANNOUNCEMENT"
+                Msg = "cc65 Announcement"
+            case else
+                Msg = Key
+        end select
+    end if
+    GetMsg = Msg
+end function
+
+
+
+'******************************************************************************
+' Format a string replacing %n specifiers in the format string F
+'******************************************************************************
+function Fmt (F, Values)
+    dim I, Count, Key, Val, Start, Pos
+    Count = UBound (Values)     ' How many values?
+    for I = Count to 0 step -1
+        Key = "%" & ToString (I)
+        select case VarType (Values (I))
+            case vbEmpty
+                Val = ""
+            case vbInteger
+                Val = ToString (Values (I))
+            case vbLong
+                Val = ToString (Values (I))
+            case vbNull
+                Val = ""
+            case vbSingle
+                Val = ToString (Values (I))
+            case vbDouble
+                Val = ToString (Values (I))
+            case vbString
+                Val = Values (I)
+            case else
+                Abort ("Internal error: Invalid conversion in Format()")
+        end select
+        F = Replace (F, Key, Val)
+    next
+    Fmt = F
+end function
+
+
+
+'******************************************************************************
+' Format a message replacing %n specifiers in the format string F
+'******************************************************************************
+function FmtMsg (Msg, Values)
+    FmtMsg = Fmt (GetMsg (Msg), Values)
+end function
+
+
+
+'******************************************************************************
+' Return an environment string. Fix up Microsofts "innovative" ideas.
+'******************************************************************************
+function GetEnv (Key)
+    dim Value
+    Value = Shell.ExpandEnvironmentStrings (Key)
+    if Value = Key then
+        GetEnv = vbNullString
+    else
+        GetEnv = Value
+    end if
+end function
+
+
+
+'******************************************************************************
+' Build a path from two components
+'******************************************************************************
+function BuildPath (Path, Name)
+    BuildPath = FSO.BuildPath (Path, Name)
+end function
+
+
+
+'******************************************************************************
+' Return true if the file with the given name exists
+'******************************************************************************
+function FileExists (Name)
+    On Error Resume Next
+    FileExists = FSO.FileExists (Name)
+end function
+
+
+
+'******************************************************************************
+' Return true if the folder with the given name exists
+'******************************************************************************
+function FolderExists (Name)
+    On Error Resume Next
+    FolderExists = FSO.FolderExists (Name)
+end function
+
+
+
+'******************************************************************************
+' Copy a file and return an error message (empty string if no error)
+'******************************************************************************
+function CopyFile (Source, Target)
+    if Right (Target, 1) <> "\" and FolderExists (Target) then
+        Target = Target & "\"
+    end if
+    On Error Resume Next
+    call FSO.CopyFile (Source, Target)
+    on error goto 0
+    CopyFile = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Create a folder and all parent folders and return an error string
+'******************************************************************************
+function CreateFolder (Path)
+
+    ' If the parent folder does not exist, try to create it
+    dim ParentFolder
+    ParentFolder = FSO.GetParentFolderName (Path)
+    if ParentFolder <> "" and not FolderExists (ParentFolder) then
+        CreateFolder (ParentFolder)
+    end if
+
+    ' Now try to create the actual folder
+    On Error Resume Next
+    FSO.CreateFolder (Path)
+    CreateFolder = Err.Description
+
+end function
+
+
+
+'******************************************************************************
+' Delete a file and return an error string
+'******************************************************************************
+function DeleteFile (Name)
+    On Error Resume Next
+    call FSO.DeleteFile (Name, true)
+    DeleteFile = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Delete a folder and return an error string
+'******************************************************************************
+function DeleteFolder (Path)
+    On Error Resume Next
+    call FSO.DeleteFolder (Path, true)
+    DeleteFolder = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Return the type of a registry entry
+'******************************************************************************
+function RegType (Value)
+
+    dim Result
+
+    ' Determine the type of the registry value. If the string contains percent
+    ' signs, use REG_EXPAND_SZ, otherwise use REG_SZ. This isn't always safe,
+    ' but there is no way to determine the type, and VBS itself is too stupid
+    ' to choose the correct type itself. Add the usual curse over Microsoft
+    ' here...
+    Result = InStr (1, Value, "%")
+    if Result = 0 then
+       RegType = "REG_SZ"
+    else
+       RegType = "REG_EXPAND_SZ"
+    end if
+end function
+
+
+
+'******************************************************************************
+' Read a string from the registry. Return an empty string if nothing was found.
+'******************************************************************************
+function RegReadStr (Key)
+    On Error Resume Next
+    RegReadStr = Shell.RegRead (Key)
+    if Err.Number <> 0 then
+        RegReadStr = ""
+    end if
+end function
+
+
+
+'******************************************************************************
+' Write a binary value to the registry, return an error description
+'******************************************************************************
+function RegWriteBin (Key, Value)
+    on error resume next
+    Shell.RegWrite Key, Value, "REG_BINARY"
+    RegWriteBin = Err.Description
+    on error goto 0
+    WriteUninstallCtrlFile ("R " & Key)
+end function
+
+
+
+'******************************************************************************
+' Write a string value to the registry, return an error description
+'******************************************************************************
+function RegWriteStr (Key, Value)
+    on error resume next
+    Shell.RegWrite Key, Value, "REG_SZ"
+    RegWriteStr = Err.Description
+    on error goto 0
+    WriteUninstallCtrlFile ("R " & Key)
+end function
+
+
+
+'******************************************************************************
+' Run a program, wait for its termination and return an error code.
+'******************************************************************************
+function Run (Cmd, WinState)
+    dim ErrCode
+
+    On Error Resume Next
+    ErrCode = Shell.Run (Cmd, WinState, true)
+    if Err.Number <> 0 then
+        ErrCode = Err.Number
+    end if
+    Run = ErrCode
+end function
+
+
+
+'******************************************************************************
+' Display a progress bar using the internet exploder
+'******************************************************************************
+dim PBDoc               ' Progress bar document object
+dim PBVal               ' Current progress bar setting
+dim IEApp               ' Internet exploder application object
+set PBDoc = nothing
+set IEApp = nothing
+PBVal = -1
+
+sub ProgressBar (Percent)
+
+    ' Remember the last setting
+    PBVal = Percent
+
+    'Create the progress bar window
+    if PBDoc is nothing then
+
+        if ((Cint (Percent) < 0) or (Cint (Percent) > 100)) then
+            ' Close request, but object already destroyed
+            exit sub
+        end if
+
+        ' Create an object that control the internet exploder
+        set IEApp = CreateObject ("InternetExplorer.Application")
+
+        ' Set the exploder to fullscreen and retrieve its size
+        dim ScreenHeight, ScreenWidth
+        IEApp.Visible    = false
+        IEApp.FullScreen = true
+        ScreenWidth      = IEApp.Width
+        ScreenHeight     = IEApp.Height
+        IEApp.FullScreen = false
+
+        ' Now prepare the actual window
+        IEApp.Offline    = true
+        IEApp.AddressBar = false
+        IEApp.Height     = 100
+        IEApp.Width      = 250
+        IEApp.MenuBar    = false
+        IEApp.StatusBar  = false
+        IEApp.Silent     = true
+        IEApp.ToolBar    = false
+        IEApp.Resizable  = false
+        IEApp.Left       = (ScreenWidth  - IEApp.Width)  / 2
+        IEApp.Top        = (ScreenHeight - IEApp.Height) / 2
+        call IEApp.Navigate ("about:blank")
+        do while IEApp.Busy
+            call WScript.Sleep (100)
+        loop
+
+        ' Connect to the displayed document
+        do until not PBDoc is nothing
+            call WScript.Sleep (100)
+            set PBDoc = IEApp.Document
+        loop
+
+        ' Generate a new document showing a progress bar
+        PBDoc.Open
+        call PBDoc.Write ("<html><head><title>" & Installer & " progress</title></head>")
+        call PBDoc.Write ("<body bgcolor=#C0C0C0><center>")
+        call PBDoc.Write ("<table width=100% border=1 frame=box><tr><td>")
+        call PBDoc.Write ("<table id=progress width=0 border=0 cellpadding=0 cellspacing=0 bgcolor=#FFFFFF>")
+        call PBDoc.Write ("<tr><td>&nbsp</td></tr></table>")
+        call PBDoc.Write ("</td></tr></table>")
+        call PBDoc.Write ("</center></body></html>")
+        PBDoc.Close
+
+        ' Display the exploder window
+        IEApp.Visible = true
+
+    else
+        if ((Cint (Percent) < 0) or (Cint (Percent) > 100)) then
+            ' Request for shutdown
+            IEApp.Visible = false
+            set PBDoc = nothing
+            IEApp.Quit
+            set IEApp = nothing
+        else
+            ' Update the progress bar
+            if Cint (Percent) = 0 then
+                PBDoc.all.progress.width = "1%"
+                PBDoc.all.progress.bgcolor = "#C0C0C0"
+            else
+                PBDoc.all.progress.width = Cstr (Cint (Percent)) & "%"
+                PBDoc.all.progress.bgcolor = "#0000C0"
+            end if
+        end if
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Initialize global variables
+'******************************************************************************
+sub InitializeGlobals ()
+    dim I
+
+    ' String stuff used for formatting
+    Tab     = Chr (9)
+    NewLine = Chr (13)
+
+    ' Global objects
+    set Shell = WScript.CreateObject ("WScript.Shell")
+    set FSO   = CreateObject ("Scripting.FileSystemObject")
+
+    ' Arguments
+    set ProgArgs = WScript.Arguments
+
+    ' Handle program arguments
+    Dbg = false
+    Language = "de"
+    for I = 0 to ProgArgs.Count-1
+        select case ProgArgs(I)
+            case "-de"
+                Language = "de"
+            case "-debug"
+                Dbg = true
+            case "-en"
+                Language = "en"
+        end select
+    next
+
+    ' Paths and locations
+    SystemDrive = GetEnv ("%SystemDrive%")
+    if SystemDrive = vbNullString then
+        SystemDrive = "c:"
+    end if
+    SystemRoot = GetEnv ("%SystemRoot%")
+    if SystemRoot = vbNullString then
+        SystemRoot = BuildPath (SystemDrive, "winnt")
+    end if
+    UserName = GetEnv ("%USERNAME%")
+    if UserName = vbNullString then
+        UserName = "Administrator"
+    end if
+    UserProfile = GetEnv ("%USERPROFILE%")
+    if UserProfile = vbNullString then
+        UserProfile = BuildPath (SystemDrive, "Dokumente und Einstellungen\" & UserName)
+    end if
+    ProgramFiles = GetEnv ("%ProgramFiles%")
+    if ProgramFiles = vbNullString then
+        ProgramFiles = BuildPath (SystemDrive, "Programme")
+    end if
+    AppData = GetEnv ("%AppData%")
+    if AppData = vbNullString then
+        AppData = UserProfile
+    end if
+    InstallSource = FSO.GetParentFolderName (WScript.ScriptFullName)
+    InstallTarget = BuildPath (ProgramFiles, AppName)
+
+    Programs = Shell.SpecialFolders ("AllUsersPrograms")
+    Desktop  = Shell.SpecialFolders ("AllUsersDesktop")
+
+    ' Uninstaller
+    set UninstallCtrlFile = nothing
+    Uninstaller = BuildPath (InstallTarget, "uninstall.vbs")
+    UninstallCtrlFileName = BuildPath (InstallTarget, "uninstall.lst")
+    UninstallerCmdLine = "-" & Language & " " & AppName & " " & UninstallCtrlFileName
+
+    ' Registry paths
+    RegUninstall = "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\" & AppName & "\"
+
+    ' Directories
+    BinDir = BuildPath (InstallTarget, "bin")
+    LibDir = BuildPath (InstallTarget, "lib")
+    IncDir = BuildPath (InstallTarget, "include")
+
+    ' Files
+    AnnouncementFile = "announce.txt"
+    DocIndexFile     = "doc\index.html"
+end sub
+
+
+
+'******************************************************************************
+' Ask a yes/no question and return the result. "Yes" is default.
+'******************************************************************************
+function AskYesNo (Question)
+    AskYesNo = MsgBox (Question, vbYesNo + vbQuestion + vbDefaultButton1, Installer)
+end function
+
+
+
+'******************************************************************************
+' Ask a yes/no question and return the result. "No" is default.
+'******************************************************************************
+function AskNoYes (Question)
+    AskNoYes = MsgBox (Question, vbYesNo + vbQuestion + vbDefaultButton2, Installer)
+end function
+
+
+
+'******************************************************************************
+' Tell the user that the installation was aborted and terminate the script
+'******************************************************************************
+sub InfoAbort ()
+    call MsgBox (GetMsg ("MSG_ABORTINFO"), vbOkOnly + vbInformation, Installer)
+    WScript.Quit (0)
+end sub
+
+
+
+'******************************************************************************
+' Input routine with the window caption preset
+'******************************************************************************
+function Input (Prompt, Default)
+    Input = InputBox (Prompt, Installer, Default)
+end function
+
+
+
+'******************************************************************************
+' Check if a directory is a given the path
+'******************************************************************************
+function DirInPath (ByVal Dir)
+
+    dim Path, Entries, I
+
+    ' Get the path in lower case
+    Path = GetEnv ("%Path%")
+
+    ' Convert the directory to lower case
+    Dir = LCase (Dir)
+
+    ' Split the path into separate entries
+    Entries = Split (Path, ";")
+
+    ' Check all entries
+    for I = LBound (Entries) to UBound (Entries)
+       if Entries(I) = Dir then
+           DirInPath = true
+           exit function
+       end if
+    next
+
+    DirInPath = false
+end function
+
+
+
+
+'******************************************************************************
+' Function that displays the paths and locations found
+'******************************************************************************
+function OneLoc (Key, Value)
+    dim Result
+    Result = Trim (Key)
+    if Len (Result) <= 8 then
+        Result = Result & Tab
+    end if
+    OneLoc = Result & Tab & "=" & Tab & Value & NewLine
+end function
+
+sub ShowPathsAndLocations ()
+    dim Msg
+    Msg = Msg & OneLoc ("SystemDrive",   SystemDrive)
+    Msg = Msg & OneLoc ("SystemRoot",    SystemRoot)
+    Msg = Msg & OneLoc ("UserName",      UserName)
+    Msg = Msg & OneLoc ("UserProfile",   UserProfile)
+    Msg = Msg & OneLoc ("ProgramFiles",  ProgramFiles)
+    Msg = Msg & OneLoc ("AppData",       AppData)
+    Msg = Msg & OneLoc ("InstallSource", InstallSource)
+    Msg = Msg & OneLoc ("InstallTarget", InstallTarget)
+    Msg = Msg & OneLoc ("Programs",      Programs)
+    Msg = Msg & OneLoc ("Desktop",       Desktop)
+    Msg = Msg & OneLoc ("Free space",    ToString (GetDriveSpace (InstallTarget)))
+
+    call MsgBox (Msg, vbOkOnly, "Paths and Locations")
+end sub
+
+
+
+'******************************************************************************
+' Return the amount of free space for a path (in Megabytes)
+'******************************************************************************
+function GetDriveSpace (Path)
+    dim Drive
+    On Error Resume Next
+    set Drive = FSO.GetDrive (FSO.GetDriveName (Path))
+    if Err.Number <> 0 then
+        GetDriveSpace = 0
+    else
+        GetDriveSpace = Drive.FreeSpace / (1024 * 1024)
+    end if
+end function
+
+
+
+'******************************************************************************
+' Check that were running this script as admin
+'******************************************************************************
+sub CheckAdminRights ()
+
+    ' FIXME: This check is not perfect
+    if UserName <> "Administrator" then
+        dim Args(1)
+        Args(1) = AppName
+
+        if AskNoYes (FmtMsg ("MSG_ADMIN", Args)) <> vbYes then
+            WScript.Quit (1)
+        end if
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Remove an old installation.
+'******************************************************************************
+sub RemoveOldInstall (UninstallCmd)
+
+    dim ErrCode
+
+    ' Execute the uninstall
+    ErrCode = Run (UninstallCmd, 0)
+
+    ' Tell the user that the uninstall is done
+    if ErrCode <> 0 then
+        call Abort (GetMsg ("MSG_UNINSTALLERR"))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Check if there is an old installation. Offer to remove it.
+'******************************************************************************
+sub CheckOldInstall ()
+
+    dim UninstallCmd
+
+    ' Read the uninstall command from the registry
+    UninstallCmd = RegReadStr (RegUninstall & "UninstallString")
+
+    ' Check if there is already an executable
+    if UninstallCmd <> "" then
+
+        ' Ask to remove an old install
+        if AskYesNo (GetMsg ("MSG_REMOVEOLD")) = vbYes then
+            ' Remove the old installation
+            call RemoveOldInstall (UninstallCmd)
+        end if
+
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Check that the install target exists. Offer to create it.
+'******************************************************************************
+sub CheckInstallTarget ()
+
+    dim Msg, Result, Args(2)
+
+    ' Tell the user about the install target and ask if it's ok
+    Args(1) = InstallTarget
+    Msg = FmtMsg ("MSG_INSTALLPATH", Args)
+    if MsgBox (Msg, vbOkCancel, Installer) <> vbOk then
+        call InfoAbort ()
+    end if
+
+    ' Check if there's enough space left on the target drive
+    if GetDriveSpace (InstallTarget) < SpaceNeeded then
+        Args(1) = FSO.GetDriveName (InstallTarget)
+        Args(2) = SpaceNeeded
+        call Abort (FmtMsg ("MSG_DRIVESPACE", Args))
+    end if
+
+    ' Check if the install path exist, create it if necessary
+    if not FolderExists (InstallTarget) then
+        Result = CreateFolder (InstallTarget)
+        if Result <> "" then
+            Args(1) = InstallTarget
+            Args(2) = Result
+            call Abort (FmtMsg ("MSG_CREATEERR", Args))
+        end if
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Create the uninstall control file
+'******************************************************************************
+sub CreateUninstallCtrlFile ()
+
+    dim Filename
+
+    ' Generate the filename
+    on Error resume next
+    set UninstallCtrlFile = FSO.CreateTextFile (UninstallCtrlFileName, true)
+    on error goto 0
+    if Err.Number <> 0 then
+        dim Args(2)
+        Args(1) = UninstallCtrlFileName
+        Args(2) = Err.Description
+        call ErrorMsg (FmtMsg ("MSG_CREATEERR", Args))
+        WScript.Quit (1)
+    end if
+
+    ' Write the name of the target directory to the file
+    call WriteUninstallCtrlFile ("D " & InstallTarget)
+
+    ' Write the name of the file itself to the file
+    call WriteUninstallCtrlFile ("F " & UninstallCtrlFileName)
+
+end sub
+
+
+
+'******************************************************************************
+' Write to the uninstall control file
+'******************************************************************************
+sub WriteUninstallCtrlFile (Line)
+
+    on error resume next
+    UninstallCtrlFile.WriteLine (Line)
+    if Err.Number <> 0 then
+        dim Args(2)
+        Args(1) = UninstallCtrlFileName
+        Args(2) = Err.Description
+        call ErrorMsg (FmtMsg ("MSG_WRITEERR", Args))
+        WScript.Quit (1)
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Close the uninstall control file
+'******************************************************************************
+sub CloseUninstallCtrlFile ()
+
+    on error resume next
+    UninstallCtrlFile.Close
+    if Err.Number <> 0 then
+        dim Args(2)
+        Args(1) = UninstallCtrlFileName
+        Args(2) = Err.Description
+        call ErrorMsg (FmtMsg ("MSG_WRITEERR", Args))
+        WScript.Quit (1)
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Copy the application files
+'******************************************************************************
+sub RecursiveCopy (Dir, SourcePath, TargetPath)
+
+    dim File, TargetFile, SubDir, SourceName, TargetName, Result, Args(3)
+
+    ' Copy all files in this folder
+    for each File in Dir.Files
+
+        ' Generate source and target file names
+        SourceName = BuildPath (SourcePath, File.Name)
+        TargetName = BuildPath (TargetPath, File.Name)
+
+        ' Copy the file. The error check doesn't seem to work.
+        on error resume next
+        File.Copy (TargetName)
+        on error goto 0
+        if Err.Number <> 0 then
+            Args(1) = SourceName
+            Args(2) = TargetName
+            Args(3) = Err.Description
+            call ErrorMsg (FmtMsg ("MSG_COPYERR", Args))
+            call AbortInfo ()
+        end if
+
+        ' Remove the r/o attribute from the target file if set
+        set TargetFile = FSO.GetFile (TargetName)
+        if TargetFile.Attributes mod 2 = 1 then
+            TargetFile.Attributes = TargetFile.Attributes - 1
+        end if
+
+        ' Remember this file in the uninstall control file
+        call WriteUninstallCtrlFile ("F " & TargetName)
+    next
+
+    ' Handle all subdirectories
+    for each SubDir in Dir.SubFolders
+
+        ' Update the progress bar with each copied directory
+        if PBVal <= 80 then
+            call ProgressBar (PBVal + 5)
+        end if
+
+        ' Generate the new directory names
+        SourceName = BuildPath (SourcePath, SubDir.Name)
+        TargetName = BuildPath (TargetPath, SubDir.Name)
+
+        ' Generate the new target dir. Notify the user about errors, but
+        ' otherwise ignore them.
+        Result = CreateFolder (TargetName)
+        if Result <> "" then
+            ' Display an error but try to continue
+            Args(1) = TargetName
+            Args(2) = Result
+            call ErrorMsg (FmtMsg ("MSG_CREATEERR", Args))
+        end if
+
+        ' Recursively process files in the subdirectory
+        call RecursiveCopy (SubDir, SourceName, TargetName)
+
+        ' Remember the subdirectory in the uninstall control file
+        WriteUninstallCtrlFile ("D " & TargetName)
+
+    next
+end sub
+
+sub CopyFiles ()
+
+    ' Update the progress bar
+    call ProgressBar (10)
+
+    ' Copy all files generating entries in the uninstall control file
+    call RecursiveCopy (FSO.GetFolder (InstallSource), InstallSource, InstallTarget)
+
+    ' Update the progress bar
+    call ProgressBar (90)
+end sub
+
+
+
+'******************************************************************************
+' Create the registry entries
+'******************************************************************************
+sub CreateRegEntries ()
+
+    dim Cmd
+
+    ' Create the entry in Systemsteuerung -> Software. Check if the first write
+    ' succeeds. If not, we don't have admin rights.
+    if RegWriteBin (RegUninstall, 1) <> "" then
+        call Abort (GetMsg ("MSG_REGWRITEERR"))
+    end if
+    call RegWriteStr (RegUninstall & "DisplayName", AppName & " " & Version)
+    call RegWriteStr (RegUninstall & "UninstallString", "wscript //nologo " & Uninstaller & " " & UninstallerCmdLine)
+
+end sub
+
+
+
+'******************************************************************************
+' Function that creates an URL
+'******************************************************************************
+sub CreateUrl (Name, Url, Description)
+    ' Ignore errors
+    On Error Resume Next
+
+    dim Link
+    set Link = Shell.CreateShortcut (Name)
+    Link.TargetPath = Url
+    Link.Description = Description
+    Link.Save
+
+    ' Allow errors again
+    on error goto 0
+
+    ' Write the file name to the uninstall control file
+    WriteUninstallCtrlFile ("F " & Name)
+end sub
+
+
+
+'******************************************************************************
+' Function that creates a shortcut
+'******************************************************************************
+sub CreateShortcut (Name, Exe, Args, Description)
+    ' Ignore errors
+    On Error Resume Next
+
+    dim Link
+    set Link = Shell.CreateShortcut (Name)
+    Link.TargetPath  = Exe
+    Link.Arguments   = Args
+    Link.WindowStyle = 1
+    Link.Description = Description
+    Link.WorkingDirectory = AppData
+    Link.Save
+
+    ' Allow errors again
+    on error goto 0
+
+    ' Write the file name to the uninstall control file
+    WriteUninstallCtrlFile ("F " & Name)
+end sub
+
+
+
+'******************************************************************************
+' Function that creates the menu entries
+'******************************************************************************
+sub CreateMenuEntries ()
+    dim Folder, Result, Name, Desc, Exe, Args(2)
+
+    ' Create the start menu folder.
+    Folder = BuildPath (Programs, AppName)
+    Result = CreateFolder (Folder)
+    if Result <> "" then
+        ' Display an error but try to continue
+        Args(1) = Folder
+        Args(2) = Result
+        call ErrorMsg (FmtMsg ("MSG_CREATEERR", Args))
+    end if
+
+    ' Create an uninstall shortcut in the menu folder
+    Args(1) = AppName
+    Desc = FmtMsg ("MSG_REMOVEENTRY", Args)
+    Name = BuildPath (Folder, Desc & ".lnk")
+    call CreateShortcut (Name, Uninstaller, UninstallerCmdLine, Desc)
+
+    ' Create a documentation shortcut in the menu folder
+    Args(1) = AppName
+    Desc = FmtMsg ("MSG_DOCENTRY", Args)
+    Name = BuildPath (Folder, Desc & ".url")
+    Exe  = "file://" & BuildPath (InstallTarget, DocIndexFile)
+    call CreateUrl (Name, Exe, Desc)
+
+    ' Create the shortcut to the announcement in the menu folder
+    Desc = GetMsg ("MSG_ANNOUNCEMENT")
+    Name = BuildPath (Folder, Desc & ".url")
+    Exe = "file://" & BuildPath (InstallTarget, AnnouncementFile)
+    call CreateUrl (Name, Exe, Desc)
+
+    ' Update the uninstall control file
+    call WriteUninstallCtrlFile ("D " & Folder)
+end sub
+
+
+
+'******************************************************************************
+' Add a directory to the system path
+'******************************************************************************
+sub AddToSysPath (Dir)
+
+    dim Path
+
+    ' Handle errors. Assume failure
+    on error resume next
+
+    ' Retrieve the PATH setting
+    Path = Shell.RegRead (SysPath)
+    if Err.Number <> 0 then
+       ' Could not read
+        call Abort (GetMsg ("MSG_REGREADERR"))
+    end if
+
+    ' Add the new directory to the path
+    if (Len (Path) > 0) and (Right (Path, 1) <> ";") then
+       Path = Path + ";"
+    end if
+    Path = Path + Dir
+
+    ' Write the new path
+    call Shell.RegWrite (SysPath, Path, "REG_EXPAND_SZ")
+    if Err.Number <> 0 then
+       ' Could not set
+        call Abort (GetMsg ("MSG_REGWRITEERR"))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Add environment variables
+'******************************************************************************
+sub AddEnvironment ()
+
+    ' Add CC65_LIB
+    if RegWriteStr (SysEnv & "\CC65_LIB", LibDir) <> "" then
+        call Abort (GetMsg ("MSG_REGWRITEERR"))
+    end if
+
+    ' Add CC65_INC
+    if RegWriteStr (SysEnv & "\CC65_INC", IncDir) <> "" then
+        call Abort (GetMsg ("MSG_REGWRITEERR"))
+    end if
+
+    ' Add the bin directory to the path
+    call AddToSysPath (BinDir)
+
+    ' Run the wm_settingchange program to notify other running programs
+    ' of the changed environment. Ignore errors.
+    call Run (BuildPath (BinDir, "wm_settingchange.exe"), 0)
+
+
+end sub
+
+
+
+'******************************************************************************
+' Function that tells the user that the install was successful
+'******************************************************************************
+sub Success ()
+    call MsgBox (GetMsg ("MSG_SUCCESS"), vbOkOnly + vbInformation, Installer)
+end sub
+
+
+
+'******************************************************************************
+' Main program
+'******************************************************************************
+sub Main ()
+
+    ' Initialize global variables. This includes the paths used
+    call InitializeGlobals ()
+    if Dbg then
+        call ShowPathsAndLocations ()
+    end if
+
+    ' Check that we're running this script as admin
+    call CheckAdminRights ()
+
+    ' Check if there is an old installation and offer to remove it
+    call CheckOldInstall ()
+
+    ' Check if the source directory does really exist
+    call CheckInstallTarget ()
+
+    ' Display the progress bar
+    call ProgressBar (0)
+
+    ' Create the uninstall file
+    call CreateUninstallCtrlFile ()
+    call ProgressBar (2)
+
+    ' Create registry entries
+    CreateRegEntries ()
+    call Progressbar (5)
+
+    ' Copy the application files (will do automatic progress bar updates)
+    call CopyFiles ()
+
+    ' Create the menu entries
+    call CreateMenuEntries ()
+    call ProgressBar (90)
+
+    ' Add entries to the enviroment
+    call AddEnvironment ()
+    call ProgressBar (95)
+
+    ' Close the uninstall control file
+    call CloseUninstallCtrlFile ()
+
+    ' We're done
+    call ProgressBar (100)
+    call ProgressBar (-1)
+    call Success ()
+
+    ' Return a success code
+    WScript.Quit (0)
+end sub
+
+
+
+'******************************************************************************
+' The script body just calls Main...
+'******************************************************************************
+Main ()
+
+
+
+
+
diff --git a/packages/windows/make-cc65.bat b/packages/windows/make-cc65.bat
deleted file mode 100755 (executable)
index c81f302..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-@echo off
-
-set CC65VER=2.9.0
-
-REM Cleanup old stuff
-rm -r cc65
-
-REM Create the directories
-mkdir cc65 > nul
-mkdir cc65\bin > nul
-
-REM Copy stuff from trixie
-for %i in (asminc doc emd include lib tgi) do (
-    if not exist u:\cc65\%i (
-       echo u:\cc65\%i does not exist
-       quit 1
-    )
-    mkdir cc65\%i
-    xcopy /q /s u:\cc65\%i\* cc65\%i
-)
-
-move /q cc65\doc\announce.txt cc65
-ren /q cc65\doc\samples cc65\samples
-
-REM Check if the compiler source exists
-if not exist c:\uz\src\cc65 (
-    echo c:\uz\src\cc65 does not exist
-    quit 1
-)
-
-for %i in (apple2 atari c16 c64 c128 cbm510 cbm610 geos pet plus4 vic20) do (
-    setlocal
-    cd cc65
-    if exist u:\cc65\cc65-%i-%CC65VER%.zip del /q u:\cc65\cc65-%i-%CC65VER%.zip
-    zip -q9m u:\cc65\cc65-%i-%CC65VER%.zip lib\%i.lib lib\%i.o include\%i.h doc\%i.cfg
-    if exist emd\%i*.emd zip -q9m u:\cc65\cc65-%i-%CC65VER%.zip emd\%i*.emd
-    if exist tgi\%i*.tgi zip -q9m u:\cc65\cc65-%i-%CC65VER%.zip tgi\%i*.tgi
-    endlocal
-)
-
-REM Specials for the GEOS package
-cd cc65
-zip -q9mr u:\cc65\cc65-geos-%CC65VER%.zip include\geos samples\geos
-cd ..
-
-REM Generate the win32 package
-(
-    setlocal
-    cd src\cc65\src
-    gmake -f make\watcom.mak clean
-    gmake -f make\watcom.mak
-    gmake -f make\watcom.mak strip
-    endlocal
-)
-
-del /q /y cc65\bin\*
-copy /q src\cc65\src\ar65\ar65.exe cc65\bin
-copy /q src\cc65\src\ca65\ca65.exe cc65\bin
-copy /q src\cc65\src\cc65\cc65.exe cc65\bin
-copy /q src\cc65\src\cl65\cl65.exe cc65\bin
-copy /q src\cc65\src\da65\da65.exe cc65\bin
-copy /q src\cc65\src\grc\grc.exe cc65\bin
-copy /q src\cc65\src\ld65\ld65.exe cc65\bin
-copy /q src\cc65\src\od65\od65.exe cc65\bin
-if exist u:\cc65\cc65-win32-%CC65VER%.zip del /q u:\cc65\cc65-win32-%CC65VER%.zip
-cd cc65
-zip -q9r u:\cc65\cc65-win32-%CC65VER%.zip *
-cd ..
-
-REM Generate the os2 package
-(
-    setlocal
-    cd src\cc65\src
-    gmake -f make\watcom.mak clean
-    gmake -f make\watcom.mak os2
-    gmake -f make\watcom.mak strip
-    endlocal
-)
-
-del /q /y cc65\bin\*
-copy /q src\cc65\src\ar65\ar65.exe cc65\bin
-copy /q src\cc65\src\ca65\ca65.exe cc65\bin
-copy /q src\cc65\src\cc65\cc65.exe cc65\bin
-copy /q src\cc65\src\cl65\cl65.exe cc65\bin
-copy /q src\cc65\src\da65\da65.exe cc65\bin
-copy /q src\cc65\src\grc\grc.exe cc65\bin
-copy /q src\cc65\src\ld65\ld65.exe cc65\bin
-copy /q src\cc65\src\od65\od65.exe cc65\bin
-if exist u:\cc65\cc65-os2-%CC65VER%.zip del /q u:\cc65\cc65-os2-%CC65VER%.zip
-cd cc65
-zip -q9r u:\cc65\cc65-os2-%CC65VER%.zip *
-cd ..
-
-REM Generate the dos32 package
-(
-    setlocal
-    cd src\cc65\src
-    gmake -f make\watcom.mak clean
-    gmake -f make\watcom.mak dos
-    gmake -f make\watcom.mak strip
-    endlocal
-)
-
-del /q /y cc65\bin\*
-copy /q src\cc65\src\ar65\ar65.exe cc65\bin
-copy /q src\cc65\src\ca65\ca65.exe cc65\bin
-copy /q src\cc65\src\cc65\cc65.exe cc65\bin
-copy /q src\cc65\src\cl65\cl65.exe cc65\bin
-copy /q src\cc65\src\da65\da65.exe cc65\bin
-copy /q src\cc65\src\grc\grc.exe cc65\bin
-copy /q src\cc65\src\ld65\ld65.exe cc65\bin
-copy /q src\cc65\src\od65\od65.exe cc65\bin
-copy /q \watcom\binw\dos4gw.exe cc65\bin
-if exist u:\cc65\cc65-dos32-%CC65VER%.zip del /q u:\cc65\cc65-dos32-%CC65VER%.zip
-cd cc65
-zip -q9r u:\cc65\cc65-dos32-%CC65VER%.zip *
-cd ..
-
-
-
diff --git a/packages/windows/uninstall.vbs b/packages/windows/uninstall.vbs
new file mode 100644 (file)
index 0000000..4349a97
--- /dev/null
@@ -0,0 +1,679 @@
+Option Explicit                 ' Variables must be declared explicitly
+
+
+
+'******************************************************************************
+' Global constants and variables
+'******************************************************************************
+dim Tab, NewLine                ' String constants
+dim Shell, FSO                  ' Global objects
+dim ProgArgs                    ' Program arguments
+dim Dbg                         ' Output debugging stuff
+dim Language                    ' Program language
+dim AppName                     ' Application name
+dim Title                       ' Application title
+dim UninstallCtrlFileName       ' Name of the uninstall control file
+dim SystemDrive                 ' The system drive
+dim SystemRoot                  ' The windows directory
+dim UserName                    ' Name if the current user
+dim UserProfile                 ' User profile directory
+dim ProgramFiles                ' Program files directory
+dim Failed                      ' Global flag for removal failed
+dim RegList                     ' List of registry entries to remove
+dim FileList                    ' List of files to remove
+dim DirList                     ' List of directories to remove
+
+
+
+'******************************************************************************
+' Display an error message window with an OK button
+'******************************************************************************
+sub ErrorMsg (Msg)
+    call MsgBox (Msg, vbOkOnly + vbExclamation, Title)
+end sub
+
+
+
+'******************************************************************************
+' Display an error message window and abort the installer
+'******************************************************************************
+sub Abort (Msg)
+    call ErrorMsg (Msg)
+    WScript.Quit (1)
+end sub
+
+
+
+'******************************************************************************
+' Convert a number to a string
+'******************************************************************************
+function ToString (Num)
+    ToString = FormatNumber (Num, vbFalse, vbTrue, vbFalse, vbFalse)
+end function
+
+
+
+'******************************************************************************
+' Return a message in the current language
+'******************************************************************************
+function GetMsg (Key)
+    dim Msg
+
+    ' Handle other languages here
+
+    ' Default is english
+    if IsEmpty (Msg) then
+        ' No assignment, use english
+        select case Key
+            case "MSG_ABORT"
+                Msg = "Do you want to abort the installation?"
+            case "MSG_ADMIN"
+                Msg = "You must be Administrator to remove %1."
+                Msg = Msg & " Are you sure you want to continue?"
+            case "MSG_CTRLFILEERR"
+                Msg = "The file %1 is invalid." & NewLine
+                Msg = Msg & "Line %2: %3"
+            case "MSG_DIRDEL"
+                Msg = "Some folders could not be removed:"
+                Msg = Msg & NewLine & "%1"
+            case "MSG_DUPLICATE"
+                Msg = "Duplicate value"
+            case "MSG_FAILURE"
+                Msg = "Could not remove %1." & NewLine
+                Msg = "%2 needs to be run by an Administrator!"
+            case "MSG_FILEDEL"
+                Msg = "Some files could not be deleted:"
+                Msg = Msg & NewLine & "%1"
+            case "MSG_OPENERR"
+                Msg = "Error opening %1"
+            case "MSG_REGDEL"
+                Msg = "Some registry entries could not be deleted:"
+                Msg = Msg & NewLine & "%1"
+            case "MSG_REMOVE"
+                Msg = "Remove %1?"
+            case "MSG_SUCCESS"
+                Msg = "%1 has been successfully removed."
+            case "MSG_USAGE"
+                Msg = "Usage:" & NewLine & "uninstall appname ctrl-file"
+            case else
+                Msg = Key
+        end select
+    end if
+    GetMsg = Msg
+end function
+
+
+
+'******************************************************************************
+' Format a string replacing %n specifiers in the format string F
+'******************************************************************************
+function Fmt (F, Values)
+    dim I, Count, Key, Val, Start, Pos
+    Count = UBound (Values)     ' How many values?
+    for I = Count to 0 step -1
+        Key = "%" & ToString (I)
+        select case VarType (Values (I))
+            case vbEmpty
+                Val = ""
+            case vbInteger
+                Val = ToString (Values (I))
+            case vbLong
+                Val = ToString (Values (I))
+            case vbNull
+                Val = ""
+            case vbSingle
+                Val = ToString (Values (I))
+            case vbDouble
+                Val = ToString (Values (I))
+            case vbString
+                Val = Values (I)
+            case else
+                Abort ("Internal error: Invalid conversion in Format()")
+        end select
+        F = Replace (F, Key, Val)
+    next
+    Fmt = F
+end function
+
+
+
+'******************************************************************************
+' Format a message replacing %n specifiers in the format string F
+'******************************************************************************
+function FmtMsg (Msg, Values)
+    FmtMsg = Fmt (GetMsg (Msg), Values)
+end function
+
+
+
+'******************************************************************************
+' Return an environment string. Fix up Microsoft "innovative" ideas.
+'******************************************************************************
+function GetEnv (Key)
+    dim Value
+    Value = Shell.ExpandEnvironmentStrings (Key)
+    if Value = Key then
+        GetEnv = vbNullString
+    else
+        GetEnv = Value
+    end if
+end function
+
+
+
+'******************************************************************************
+' Build a path from two components
+'******************************************************************************
+function BuildPath (Path, Name)
+    BuildPath = FSO.BuildPath (Path, Name)
+end function
+
+
+
+'******************************************************************************
+' Delete a folder and return an error string
+'******************************************************************************
+function DeleteFolder (Path)
+    on error resume next
+    call FSO.DeleteFolder (Path, true)
+    DeleteFolder = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Delete a file and return an error string
+'******************************************************************************
+function DeleteFile (Path)
+    on error resume next
+    call FSO.DeleteFile (Path, true)
+    DeleteFile = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Delete a registry entry
+'******************************************************************************
+function RegDelete (Key)
+    on error resume next
+    call Shell.RegDelete (Key)
+    RegDelete = Err.Description
+end function
+
+
+
+'******************************************************************************
+' Sort an array of strings
+'******************************************************************************
+sub QS (byref A, Lo, Hi)
+
+    dim I, J, T
+
+    ' Quicksort
+    do while Hi > Lo
+               I = Lo + 1
+               J = Hi
+               do while I <= J
+                   do while I <= J
+                if StrComp (A(Lo), A(I), vbTextCompare) < 0 then
+                    exit do
+                end if
+                       I = I + 1
+                   loop
+                   do while I <= J
+                if StrComp (A(Lo), A(J), vbTextCompare) >= 0 then
+                    exit do
+                end if
+                       J = J - 1
+                   loop
+                   if I <= J then
+                ' Swap A(I) and A(J)
+                T = A(I)
+                A(I) = A(J)
+                A(J) = T
+                       I = I + 1
+                       J = J - 1
+                   end if
+       loop
+       if J <> Lo then
+            ' Swap A(J) and A(Lo)
+            T = A(J)
+            A(J) = A(Lo)
+            A(Lo) = T
+       end if
+               if (2 * J) > (Hi + Lo) then
+           call QS (A, J + 1, Hi)
+           Hi = J - 1
+       else
+           call QS (A, Lo, J - 1)
+           Lo = J + 1
+       end if
+    loop
+end sub
+
+sub Quicksort (byref A)
+    if UBound (A) > 1 then
+        call QS (A, LBound (A), UBound (A))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Initialize global variables
+'******************************************************************************
+sub InitializeGlobals ()
+    dim I
+
+    ' String stuff used for formatting
+    Tab     = Chr (9)
+    NewLine = Chr (13)
+
+    ' Global objects
+    set Shell = WScript.CreateObject ("WScript.Shell")
+    set FSO   = CreateObject ("Scripting.FileSystemObject")
+
+    ' Arguments
+    set ProgArgs = WScript.Arguments
+
+    ' Handle program arguments
+    AppName = ""
+    Title = "Uninstaller"
+    UninstallCtrlFileName = ""
+    Dbg = false
+    Language = "de"
+    for I = 0 to ProgArgs.Count-1
+        select case ProgArgs(I)
+            case "-de"
+                Language = "de"
+            case "-debug"
+                Dbg = true
+            case "-en"
+                Language = "en"
+            case else
+                if AppName = "" then
+                    AppName = ProgArgs(I)
+                elseif UninstallCtrlFileName = "" then
+                    UninstallCtrlFileName = ProgArgs(I)
+                else
+                    call ErrorMsg (GetMsg ("MSG_USAGE"))
+                    WScript.Quit (1)
+                end if
+        end select
+    next
+
+    ' We need the application name and uninstall control file
+    if AppName = "" or UninstallCtrlFileName = "" then
+        call Abort (GetMsg ("MSG_USAGE"))
+    end if
+
+    ' Set the title early, because it's used in error messages
+    Title = AppName & " Uninstaller"
+
+    ' Paths and locations
+    SystemDrive = GetEnv ("%SystemDrive%")
+    if SystemDrive = vbNullString then
+        SystemDrive = "c:"
+    end if
+    SystemRoot = GetEnv ("%SystemRoot%")
+    if SystemRoot = vbNullString then
+        SystemRoot = BuildPath (SystemDrive, "winnt")
+    end if
+    UserName = GetEnv ("%USERNAME%")
+    if UserName = vbNullString then
+        UserName = "Administrator"
+    end if
+    UserProfile = GetEnv ("%USERPROFILE%")
+    if UserProfile = vbNullString then
+        UserProfile = BuildPath (SystemDrive, "Dokumente und Einstellungen\" & UserName)
+    end if
+    ProgramFiles = GetEnv ("%ProgramFiles%")
+    if ProgramFiles = vbNullString then
+        ProgramFiles = BuildPath (SystemDrive, "Programme")
+    end if
+
+    ' Assume we could remove the software
+    Failed = false
+
+end sub
+
+
+
+'******************************************************************************
+' Ask a yes/no question and return the result. "Yes" is default.
+'******************************************************************************
+function AskYesNo (Question)
+    AskYesNo = MsgBox (Question, vbYesNo + vbQuestion + vbDefaultButton1, Title)
+end function
+
+
+
+'******************************************************************************
+' Ask a yes/no question and return the result. "No" is default.
+'******************************************************************************
+function AskNoYes (Question)
+    AskNoYes = MsgBox (Question, vbYesNo + vbQuestion + vbDefaultButton2, Title)
+end function
+
+
+
+'******************************************************************************
+' Ask if the user wants to abort install, and terminate if the answer is yes
+'******************************************************************************
+sub QueryAbort ()
+    if AskNoYes (GetMsg ("MSG_ABORT")) = vbYes then
+        WScript.Quit (1)
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Function that displays the paths and locations found
+'******************************************************************************
+function OneLoc (Key, Value)
+    dim Result
+    Result = Trim (Key)
+    if Len (Result) <= 8 then
+        Result = Result & Tab
+    end if
+    OneLoc = Result & Tab & "=" & Tab & Value & NewLine
+end function
+
+sub ShowPathsAndLocations ()
+    dim Msg
+    Msg = Msg & OneLoc ("SystemDrive",   SystemDrive)
+    Msg = Msg & OneLoc ("SystemRoot",    SystemRoot)
+    Msg = Msg & OneLoc ("UserName",      UserName)
+    Msg = Msg & OneLoc ("UserProfile",   UserProfile)
+    Msg = Msg & OneLoc ("ProgramFiles",  ProgramFiles)
+
+    MsgBox Msg, vbOkOnly, "Paths and Locations"
+end sub
+
+
+
+'******************************************************************************
+' Check that were running this script as admin
+'******************************************************************************
+sub CheckAdminRights ()
+
+    ' FIXME: This check is not perfect
+    if UserName <> "Administrator" then
+        dim Args(1)
+        Args(1) = AppName
+
+        if AskNoYes (FmtMsg ("MSG_ADMIN", Args)) <> vbYes then
+            WScript.Quit (1)
+        end if
+    end if
+
+end sub
+
+
+
+'******************************************************************************
+' Read the uninstall control file and create the data collections
+'******************************************************************************
+sub InvalidCtrlFile (Line, Val)
+    dim Args(3)
+    Args(1) = UninstallCtrlFileName
+    Args(2) = Line
+    Args(3) = Val
+    call Abort (FmtMsg ("MSG_CTRLFILEERR", Args))
+end sub
+
+sub ReadUninstallCtrlFile ()
+
+    const ForReading = 1
+    dim File, Line, Tag, Args(3)
+    dim MyRegList, MyFileList, myDirList
+
+    ' Create some dictionaries. These are not really used as dictionaries, but
+    ' have the nice property of expanding dynamically, and we need that.
+    set MyRegList  = CreateObject ("Scripting.Dictionary")
+    set MyFileList = CreateObject ("Scripting.Dictionary")
+    set MyDirList  = CreateObject ("Scripting.Dictionary")
+
+    ' Open the file. Checking Err doesn't work here, don't know why.
+    set File = nothing
+    on error resume next
+    set File = FSO.OpenTextFile (UninstallCtrlFileName, ForReading)
+    on error goto 0
+    if File is nothing then
+        Args(1) = UninstallCtrlFileName
+        call Abort (FmtMsg ("MSG_OPENERR", Args))
+    end if
+
+    ' Read all lines and insert them in their list
+    do while File.AtEndOfStream <> true
+
+        ' Read the next line
+        on error resume next
+        Line = File.ReadLine
+        on error goto 0
+
+        ' Get the type from the line and remove it, so the line contains just
+        ' the argument name
+        Tag  = Left (Line, 1)
+        Line = Mid (Line, 3)
+
+        ' Determine the type of the entry
+        select case Tag
+
+            case "D"
+                ' A directory. Convert to lowercase to unify names.
+                Line = LCase (Line)
+                if MyDirList.Exists (Line) then
+                    call InvalidCtrlFile (File.Line - 1, GetMsg ("MSG_DUPLICATE"))
+                else
+                    call MyDirList.Add (Line, "")
+                end if
+
+            case "F"
+                ' A file. Convert to lowercase to unify names
+                Line = LCase (Line)
+                if MyFileList.Exists (Line) then
+                    call InvalidCtrlFile (File.Line - 1, GetMsg ("MSG_DUPLICATE"))
+                else
+                    call MyFileList.Add (Line, "")
+                end if
+
+            case "R"
+                ' A registry entry
+                if MyRegList.Exists (Line) then
+                    call InvalidCtrlFile (File.Line - 1, GetMsg ("MSG_DUPLICATE"))
+                else
+                    call MyRegList.Add (Line, "")
+                end if
+
+            case else
+                call InvalidCtrlFile (File.Line - 1, Tag & " " & Line)
+
+        end select
+
+    loop
+
+    ' Close the file
+    on error resume next
+    call File.Close ()
+    on error goto 0
+
+    ' Make the global arrays big enough for the data
+    RegList  = Array (MyRegList.Count)
+    FileList = Array (MyFileList.Count)
+    DirList  = Array (MyDirList.Count)
+
+    ' Copy the data into the global arrays
+    RegList  = MyRegList.Keys
+    FileList = MyFileList.Keys
+    DirList  = MyDirList.Keys
+
+    ' Sort all the lists. This makes sure nodes are in the array before the
+    ' leaves that depend on it. Or in other words: Top level directories and
+    ' registry entries come first. So if we delete the items starting at the
+    ' other side of the array, we will never delete a subdirectory before its
+    ' parent directory.
+    call QuickSort (RegList)
+    call QuickSort (FileList)
+    call QuickSort (DirList)
+
+end sub
+
+
+
+'******************************************************************************
+' Delete the registry entries
+'******************************************************************************
+sub DeleteRegistryEntries ()
+
+    dim I, Result, NoDel, Args(1)
+
+    NoDel = ""
+    for I = UBound (RegList) to LBound (RegList) step -1
+        Result = RegDelete (RegList (I))
+        if Result <> "" then
+            ' Remember the entries we could not delete
+            NoDel = NoDel & RegList (I) & NewLine
+        end if
+    next
+
+    if NoDel <> "" then
+        Args(1) = NoDel
+        call ErrorMsg (FmtMsg ("MSG_REGDEL", Args))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Delete the files
+'******************************************************************************
+sub DeleteFiles ()
+
+    dim I, Result, NoDel, Args(1)
+
+    NoDel = ""
+    for I = UBound (FileList) to LBound (FileList) step -1
+        Result = DeleteFile (FileList (I))
+        if Result <> "" then
+            ' Remember the files we could not delete
+            NoDel = NoDel & FileList (I) & NewLine
+        end if
+    next
+
+    if NoDel <> "" then
+        Args(1) = NoDel
+        call ErrorMsg (FmtMsg ("MSG_FILEDEL", Args))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Delete the directories
+'******************************************************************************
+sub DeleteDirectories ()
+
+    dim I, Result, NoDel, Args(1)
+
+    NoDel = ""
+    for I = UBound (DirList) to LBound (DirList) step -1
+        Result = DeleteFolder (DirList (I))
+        if Result <> "" then
+            ' Remember the directories we could not delete
+            NoDel = NoDel & DirList (I) & NewLine
+        end if
+    next
+
+    if NoDel <> "" then
+        Args(1) = NoDel
+        call ErrorMsg (FmtMsg ("MSG_DIRDEL", Args))
+    end if
+end sub
+
+
+
+'******************************************************************************
+' Function that tells the user that the install was successful
+'******************************************************************************
+sub Success ()
+    dim Args(1), App
+
+    ' Popup message
+    Args(1) = AppName
+    call MsgBox (FmtMsg ("MSG_SUCCESS", Args), vbOkOnly + vbInformation, Title)
+
+end sub
+
+
+
+'******************************************************************************
+' Function that tells the user that the uninstall failed
+'******************************************************************************
+sub Failure ()
+    dim Args(2)
+
+    ' Popup message
+    Args(1) = AppName
+    Args(2) = Title
+    ErrorMsg (FmtMsg ("MSG_FAILURE", Args))
+    WScript.Quit (1)
+
+end sub
+
+
+
+'******************************************************************************
+' Main program
+'******************************************************************************
+sub Main ()
+
+    dim Args(1)
+
+    ' Initialize global variables. This includes the paths used
+    InitializeGlobals ()
+    if Dbg then
+        ShowPathsAndLocations ()
+    end if
+
+    ' Check that we're running this script as admin
+    CheckAdminRights ()
+
+    ' Let the user make up his mind
+    Args(1) = AppName
+    if AskYesNo (FmtMsg ("MSG_REMOVE", Args)) <> vbYes then
+        WScript.Quit (1)
+    end if
+
+    ' Read the uninstall control file
+    call ReadUninstallCtrlFile ()
+
+    ' Delete the registry entries
+    call DeleteRegistryEntries ()
+
+    ' Delete all files
+    call DeleteFiles ()
+
+    ' Delete the directories
+    call DeleteDirectories ()
+
+    ' We're done
+    if Failed then
+        Failure ()
+    else
+        Success ()
+    end if
+end sub
+
+
+
+'******************************************************************************
+' The script body just calls Main...
+'******************************************************************************
+Main ()
+
+
+
+
diff --git a/packages/windows/wm_envchange.c b/packages/windows/wm_envchange.c
new file mode 100644 (file)
index 0000000..7f2f7cb
--- /dev/null
@@ -0,0 +1,14 @@
+#include <windows.h>
+
+int main (void)
+{
+    /* Broadcast the WM_SETTINGCHANGE message with the lParam argument set
+     * to a pointer to the string "Environment" (one of the many undocumented
+     * Microsoft kludges).
+     */
+    SendMessage (HWND_BROADCAST, WM_SETTINGCHANGE, NULL, (LONG) "Environment");
+    return 0;
+}
+
+
+