diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 207220ee6..ae91a067b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ name: Release # Binary builds: a self-contained IronRuby for each platform, made by -# Util/package-release.sh. Pushing a tag v* publishes them as a GitHub release (a +# Util/package-release.sh, and for Windows also an installer made from that archive by +# Util/installer/build-installer.sh. Pushing a tag v* publishes them as a GitHub release (a # pre-release while the version has a suffix such as -preview1), with # Docs/releases/.md as the notes when there is one; running the workflow by hand, or # a pull request that changes the packaging, only keeps them as workflow artifacts. @@ -18,6 +19,7 @@ on: paths: - .github/workflows/release.yml - Util/package-release.sh + - Util/installer/** workflow_dispatch: inputs: render_video: @@ -159,16 +161,55 @@ jobs: working-directory: dist run: cd ironruby-*-win-x64 && cmd //c ir.cmd Samples/Notepad/test_notepad.rb + # Inno Setup, for the installer: its release from GitHub, pinned by version and by the + # SHA-256 GitHub lists for that asset, installed for this job only. + - name: Inno Setup + if: runner.os == 'Windows' + env: + INNO_VERSION: 6.7.3 + INNO_SHA256: 9c73c3bae7ed48d44112a0f48e66742c00090bdb5bef71d9d3c056c66e97b732 + run: | + mkdir -p "$RUNNER_TEMP/innosetup-download" + setup="$RUNNER_TEMP/innosetup-download/innosetup-$INNO_VERSION.exe" + curl -fsSL -o "$setup" \ + "https://github.com/jrsoftware/issrc/releases/download/is-${INNO_VERSION//./_}/innosetup-$INNO_VERSION.exe" + echo "$INNO_SHA256 $setup" | sha256sum -c + MSYS2_ARG_CONV_EXCL='*' "$setup" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /CURRENTUSER \ + "/DIR=$(cygpath -w "$RUNNER_TEMP/innosetup")" + test -f "$RUNNER_TEMP/innosetup/ISCC.exe" + echo "ISCC=$RUNNER_TEMP/innosetup/ISCC.exe" >> "$GITHUB_ENV" + + # The installer carries the archive just made, unchanged. + - name: Installer, from the archive + if: runner.os == 'Windows' + working-directory: ironruby + run: Util/installer/build-installer.sh "$GITHUB_WORKSPACE"/dist/ironruby-*-win-x64.zip + + # Into C:\IronRuby40-x64, for this user and then for all users: ironruby, irb and + # `igem install rack` from the PATH a new terminal gets, then the uninstaller, which + # must leave no file and the PATH as it was. + - name: Install, run and uninstall + if: runner.os == 'Windows' + timeout-minutes: 15 + shell: pwsh + working-directory: dist + run: | + $test = "$env:GITHUB_WORKSPACE\ironruby\Util\installer\test-installer.ps1" + $setup = (Get-Item ironruby-*-win-x64-setup.exe).FullName + & $test $setup + if ($LASTEXITCODE) { exit $LASTEXITCODE } + & $test $setup -AllUsers + # Signed SLSA build provenance for the archive a tag publishes, with GitHub's own # actions/attest, pinned to the commit of its v4.2.2 tag: `gh attestation verify # --repo Largo/ironruby` shows that this workflow built it, from which - # commit and in which run. dist/ironruby--.* is the archive alone, not - # the directory it was packed from. + # commit and in which run. dist/ironruby--*.* is the archive, and on + # Windows the installer, -setup.exe; not the directory the archive was packed from. - name: Attest build provenance if: github.event_name == 'push' && github.ref_type == 'tag' uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-path: dist/ironruby-*-${{ matrix.rid }}.* + subject-path: dist/ironruby-*-${{ matrix.rid }}*.* - uses: actions/upload-artifact@v7 with: @@ -176,6 +217,7 @@ jobs: path: | dist/*.tar.gz dist/*.zip + dist/*-setup.exe if-no-files-found: error # media/launch is a HyperFrames composition (media/launch/README.md). A video that diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md new file mode 100644 index 000000000..73c1f2088 --- /dev/null +++ b/Docs/releases/v4.0.3.md @@ -0,0 +1,42 @@ +### Download + +Self-contained: unpack one and run `./ironruby`, `./irb.sh` and `./igem.sh` - on Windows +`ironruby.cmd`, `irb.cmd` and `igem.cmd`. No .NET to install. Or, on Windows, run the +installer. + +- **Windows x64 installer**: `ironruby-4.0.3-win-x64-setup.exe`, the zip below, installed + into `C:\IronRuby40-x64` and put on the PATH +- **Windows x64**: `ironruby-4.0.3-win-x64.zip`, with Windows Forms, WPF, `ironrubyw.exe` + and `Samples\Notepad\notepad.cmd` +- **Linux x64**: `ironruby-4.0.3-linux-x64.tar.gz` +- **macOS arm64**: `ironruby-4.0.3-osx-arm64.tar.gz`, built in CI, not tested there + +`SHA256SUMS` lists the checksums. Each file and `SHA256SUMS` has a signed build +provenance attestation, which says which workflow built it, from which commit, in which run: + +``` +gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby +``` + +### New + +- **A Windows installer**, in the style of RubyInstaller. It installs the win-x64 zip's + tree, unchanged, into `C:\IronRuby40-x64` by default, or a directory you choose, for you + alone (no admin rights) or for all users, and puts that directory first on the PATH, so + `ironruby`, `ir`, `irb` and `igem` work from a new terminal. `igem install` writes into + the install tree, which is why the default is a directory you can write to and has no + space in its path: Bundler's `RUBYOPT`, the Rails templates and mspec break on one. + Running `.rb` files with IronRuby is an option, off by default. The uninstaller removes + the files, the gems installed since, and the PATH entry. It is built in CI with Inno + Setup 6.7.3 from `Util/installer/ironruby.iss`, and installed, run and uninstalled there + before it is published. + +### Fixes + +- **irb starts on Windows again.** In 4.0.2 it stopped at once with `unknown symbol + "FillConsoleOutputCharacter"`. reline asks Fiddle for Win32 functions by their bare + names, and Fiddle now also looks for the `A` export, as CRuby's does. In a console, irb + then crashed: reline hands the console API its buffers as `[str].pack("p")`, and on + IronRuby that was an index, not an address. It is a real address now, of a block holding + the String's bytes, and what C writes there through Fiddle comes back into the String, as + with CRuby - so reline runs as Ruby ships it. diff --git a/README.md b/README.md index 6e81e171f..a48bf4bff 100644 --- a/README.md +++ b/README.md @@ -513,7 +513,16 @@ $ ./igem.sh install rack From 4.0.2 on, the archives have `ironruby` and `ironruby.cmd` (see above), and `ir.sh` / `ir.cmd` keep working; 4.0.1 and earlier have only those. On Windows, unzip it and use `ironruby.cmd`, `irb.cmd` and `igem.cmd`, or double-click -`Samples\Notepad\notepad.cmd`. The archive has the same layout as a checkout, with the +`Samples\Notepad\notepad.cmd`. From 4.0.3 on there is also an installer, +`ironruby--win-x64-setup.exe`, in the style of RubyInstaller: it installs the same +tree into `C:\IronRuby40-x64` (or a directory you choose), for you or for all users, puts +that directory first on your PATH so `ironruby`, `ir`, `irb` and `igem` work from a new +terminal, and its uninstaller removes it all again, installed gems included. It needs no +admin rights for you alone, and keeps the default path free of spaces, which Bundler, the +Rails templates and mspec do not handle. IronRuby's `irb` then comes before CRuby's on the +PATH; CRuby's `ruby` and `gem` are not affected. `Util/installer/build-installer.sh` makes +it from the zip with [Inno Setup](https://jrsoftware.org/isinfo.php), from +`Util/installer/ironruby.iss`. The archive has the same layout as a checkout, with the Release build in `Src/Console/bin/Release/net8.0`, so everything above applies to it unchanged. `irubyc` works too, but it still needs the .NET SDK to compile the application. The Windows archive also carries the Windows Desktop runtime, for Windows Forms and WPF, and @@ -521,11 +530,13 @@ The Windows archive also carries the Windows Desktop runtime, for Windows Forms bundles, Microsoft's license and third-party notices. `Util/package-release.sh ` builds such an archive from a checkout, and -`.github/workflows/release.yml` builds all three and publishes them when a `v*` tag is -pushed; the [launch video](media/launch) is rendered and added by hand. +`.github/workflows/release.yml` builds all three, and the Windows installer from the zip, +installs and uninstalls it once for the current user and once for all users, and +publishes them when a `v*` tag is pushed; the [launch video](media/launch) is rendered and +added by hand. Releases from 4.0.2 on carry a signed [build provenance attestation](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds) -for each archive and for `SHA256SUMS`, made by GitHub's `actions/attest` in the job that +for each archive (from 4.0.3 on, the installer too) and for `SHA256SUMS`, made by GitHub's `actions/attest` in the job that built the file. It says which workflow built the file, from which commit, in which run, and that the file is the one it built. Check one with the GitHub CLI: diff --git a/Util/installer/build-installer.sh b/Util/installer/build-installer.sh new file mode 100755 index 000000000..77108c920 --- /dev/null +++ b/Util/installer/build-installer.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Builds IronRuby's Windows installer (ironruby.iss, Inno Setup 6) from the win-x64 +# release archive that Util/package-release.sh makes: +# +# Util/installer/build-installer.sh dist/ironruby-4.0.2-win-x64.zip +# -> dist/ironruby-4.0.2-win-x64-setup.exe +# ISCC=/path/to/ISCC.exe Util/installer/build-installer.sh ZIP OUTDIR +# +# The installer carries the archive's tree as it is: the archive is unpacked into a +# directory of its own and handed to Inno Setup's compiler, and the version comes from +# the archive's name. ISCC is taken from $ISCC, from PATH or from where Inno Setup 6 +# installs itself. Runs in Git Bash on Windows. +set -euo pipefail + +ZIP=${1:?usage: Util/installer/build-installer.sh ironruby-VERSION-win-x64.zip [OUTDIR]} +ISS=$(cd "$(dirname "$0")" && pwd)/ironruby.iss +ZIP=$(cd "$(dirname "$ZIP")" && pwd)/$(basename "$ZIP") +OUT=$(mkdir -p "${2:-$(dirname "$ZIP")}" && cd "${2:-$(dirname "$ZIP")}" && pwd) +NAME=$(basename "$ZIP" .zip) +case "$NAME" in + ironruby-*-win-x64) ;; + *) echo "build-installer.sh: not a win-x64 release archive: $ZIP" >&2; exit 1 ;; +esac +VERSION=${NAME#ironruby-} +VERSION=${VERSION%-win-x64} +# 4.0.3-preview1 and 4.0.2+1a2b3c4 are 4.0.3 and 4.0.2 to Windows' file version. +NUMERIC=$(echo "$VERSION" | grep -oE '^[0-9]+(\.[0-9]+){0,3}') + +if [ -z "${ISCC:-}" ]; then + for candidate in "$(command -v iscc || true)" \ + "$(cygpath -u "${LOCALAPPDATA:-C:\\}")/Programs/Inno Setup 6/ISCC.exe" \ + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" \ + "/c/Program Files/Inno Setup 6/ISCC.exe"; do + if [ -n "$candidate" ] && [ -f "$candidate" ]; then ISCC=$candidate; break; fi + done +fi +if [ -z "${ISCC:-}" ]; then + echo "build-installer.sh: no Inno Setup 6 compiler (ISCC.exe); install Inno Setup 6 or set ISCC" >&2 + exit 1 +fi + +# Unpacked into the temporary directory, not next to the output: ISCC does not read paths +# past Windows' 260 characters, and the tree's own go 100 deep. +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT +if command -v unzip >/dev/null; then unzip -q "$ZIP" -d "$STAGE" +else 7z x -bd -o"$STAGE" "$ZIP" >/dev/null; fi +test -f "$STAGE/$NAME/ironruby.cmd" + +# Not MSYS's path conversion: it would take ISCC's /D and /O switches for paths. +MSYS2_ARG_CONV_EXCL='*' "$ISCC" /Q \ + "/DAppVersion=$VERSION" "/DNumericVersion=$NUMERIC" \ + "/DSourceDir=$(cygpath -w "$STAGE/$NAME")" \ + "/O$(cygpath -w "$OUT")" "$(cygpath -w "$ISS")" +echo "installer: $OUT/$NAME-setup.exe" diff --git a/Util/installer/ironruby.iss b/Util/installer/ironruby.iss new file mode 100644 index 000000000..fa8fa0819 --- /dev/null +++ b/Util/installer/ironruby.iss @@ -0,0 +1,286 @@ +; IronRuby's Windows installer, in the style of RubyInstaller, for Inno Setup 6. +; +; Util/installer/build-installer.sh builds it from the win-x64 release archive that +; Util/package-release.sh makes, and installs the archive's tree unchanged: +; +; Util/installer/build-installer.sh dist/ironruby-4.0.2-win-x64.zip +; -> dist/ironruby-4.0.2-win-x64-setup.exe +; +; It installs into C:\IronRuby40-x64 by default, as RubyInstaller installs into +; C:\Ruby40-x64: `igem install` writes the gems into the install tree +; (Src\StdLib\ruby\gems) and their binstubs next to ir.exe, so the directory must be one +; the user can write to without admin rights, and a path with a space in it breaks +; Bundler's RUBYOPT, Rails' templates and mspec. Setup asks whether to install for the +; current user or for all users; either way it can add the install directory, where +; ironruby.cmd, ir.cmd, irb.cmd and igem.cmd are, to the front of that user's or the +; system's PATH. The uninstaller removes the files, whatever IronRuby wrote under Src +; since (installed gems included), and the PATH entry. +; +; build-installer.sh passes these; to run ISCC by hand, pass them the same way: +; /DAppVersion=4.0.2 the version, as in the archive's name +; /DNumericVersion=4.0.2 its leading numbers, for the setup program's file version +; /DSourceDir=C:\...\ironruby-4.0.2-win-x64 the unpacked archive + +#ifndef AppVersion + #error Pass /DAppVersion=, as Util/installer/build-installer.sh does +#endif +#ifndef SourceDir + #error Pass /DSourceDir=, as Util/installer/build-installer.sh does +#endif +#ifndef NumericVersion + #define NumericVersion "0.0.0" +#endif + +; "40" for 4.0.x, as in RubyInstaller's C:\Ruby40-x64: one install per Ruby series, +; side by side with the others, upgraded in place within it. +#define VersionMajor Copy(AppVersion, 1, Pos(".", AppVersion) - 1) +#define AfterMajor Copy(AppVersion, Pos(".", AppVersion) + 1) +#define VersionMinor Copy(AfterMajor, 1, Pos(".", AfterMajor) - 1) +#define Series VersionMajor + VersionMinor + +[Setup] +AppId=IronRuby{#Series}-x64 +AppName=IronRuby {#VersionMajor}.{#VersionMinor} (x64) +AppVersion={#AppVersion} +AppVerName=IronRuby {#AppVersion} (x64) +AppPublisher=IronRuby +AppPublisherURL=https://github.com/Largo/ironruby +AppSupportURL=https://github.com/Largo/ironruby/issues +AppUpdatesURL=https://github.com/Largo/ironruby/releases +VersionInfoVersion={#NumericVersion} +VersionInfoProductTextVersion={#AppVersion} +DefaultDirName={sd}\IronRuby{#Series}-x64 +DefaultGroupName=IronRuby {#VersionMajor}.{#VersionMinor} (x64) +DisableProgramGroupPage=yes +; Install for me only (the default, no admin rights) or for all users: the dialog asks, +; and /CURRENTUSER or /ALLUSERS answers it on the command line. +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog commandline +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +LicenseFile={#SourceDir}\License\License.Rtf +ChangesEnvironment=yes +ChangesAssociations=yes +UninstallDisplayName=IronRuby {#AppVersion} (x64) +OutputBaseFilename=ironruby-{#AppVersion}-win-x64-setup +Compression=lzma2/max +SolidCompression=yes +LZMANumBlockThreads=4 +WizardStyle=modern +SetupLogging=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "modifypath"; Description: "Add IronRuby to the &PATH: ironruby, ir, irb, igem" +Name: "assocfiles"; Description: "Run .&rb files with IronRuby when they are opened"; Flags: unchecked + +[Files] +; The archive's tree, unchanged. +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\Interactive IronRuby"; Filename: "{app}\irb.cmd"; WorkingDir: "{%USERPROFILE}" +Name: "{group}\Start Command Prompt with IronRuby"; Filename: "{cmd}"; Parameters: "/k ""set ""PATH={app};%PATH%"" & ironruby.cmd -v"""; WorkingDir: "{%USERPROFILE}" +Name: "{group}\IronRuby Notepad"; Filename: "{app}\Samples\Notepad\notepad.cmd"; WorkingDir: "{app}\Samples\Notepad" + +[Registry] +Root: HKA; Subkey: "Software\Classes\IronRuby.File"; ValueType: string; ValueData: "Ruby program"; Flags: uninsdeletekey; Tasks: assocfiles +Root: HKA; Subkey: "Software\Classes\IronRuby.File\shell\open\command"; ValueType: string; ValueData: """{app}\ironruby.cmd"" ""%1"" %*"; Tasks: assocfiles +; .rb itself is set in [Code], AssociateRb, which keeps the program it had before. +Root: HKA; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "IronRuby.File"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assocfiles + +[UninstallDelete] +; Everything under Src is IronRuby's, also what it wrote there after the install: the +; gems igem installed into Src\StdLib\ruby\gems and their binstubs next to ir.exe. +; Setup refuses a directory that already holds other files, so this is never someone +; else's Src. +Type: filesandordirs; Name: "{app}\Src" +Type: filesandordirs; Name: "{app}\Samples" +Type: filesandordirs; Name: "{app}\License" + +[Code] +const + SystemEnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; + UserEnvironmentKey = 'Environment'; + +{ The PATH of the user installing, or the system's when installing for all users. } +function EnvironmentRoot: Integer; +begin + if IsAdminInstallMode then + Result := HKEY_LOCAL_MACHINE + else + Result := HKEY_CURRENT_USER; +end; + +function EnvironmentKey: String; +begin + if IsAdminInstallMode then + Result := SystemEnvironmentKey + else + Result := UserEnvironmentKey; +end; + +function SameDir(Entry, Dir: String): Boolean; +begin + Result := CompareText(RemoveBackslashUnlessRoot(Trim(Entry)), RemoveBackslashUnlessRoot(Dir)) = 0; +end; + +{ Path without its entries for Dir; every other entry, empty ones too, stays as it was. } +function PathWithout(Path, Dir: String): String; +var + Rest, Entry: String; + Separator: Integer; + First, Last: Boolean; +begin + Result := ''; + Rest := Path; + First := True; + repeat + Separator := Pos(';', Rest); + Last := Separator = 0; + if Last then + Entry := Rest + else + begin + Entry := Copy(Rest, 1, Separator - 1); + Rest := Copy(Rest, Separator + 1, Length(Rest)); + end; + if (Trim(Entry) = '') or not SameDir(Entry, Dir) then + begin + if not First then + Result := Result + ';'; + Result := Result + Entry; + First := False; + end; + until Last; +end; + +{ Read and written as REG_EXPAND_SZ, so entries such as %USERPROFILE%\bin stay unexpanded. } +function ReadPath: String; +begin + if not RegQueryStringValue(EnvironmentRoot, EnvironmentKey, 'Path', Result) then + Result := ''; +end; + +procedure WritePath(Path: String); +begin + if not RegWriteExpandStringValue(EnvironmentRoot, EnvironmentKey, 'Path', Path) then + SuppressibleMsgBox('Setup could not change the PATH in ' + EnvironmentKey + '.', mbError, MB_OK, IDOK) + else + Log('PATH is now: ' + Path); +end; + +{ First, as RubyInstaller puts Ruby first: irb is IronRuby's irb then. } +procedure AddToPath(Dir: String); +var + Path: String; +begin + Path := PathWithout(ReadPath, Dir); + if Path = '' then + WritePath(Dir) + else + WritePath(Dir + ';' + Path); +end; + +procedure RemoveFromPath(Dir: String); +var + Path, Rest: String; +begin + Path := ReadPath; + Rest := PathWithout(Path, Dir); + if Rest <> Path then + WritePath(Rest); +end; + +function DirIsEmpty(Dir: String): Boolean; +var + Find: TFindRec; +begin + Result := True; + if FindFirst(AddBackslash(Dir) + '*', Find) then + try + repeat + if (Find.Name <> '.') and (Find.Name <> '..') then + begin + Result := False; + Exit; + end; + until not FindNext(Find); + finally + FindClose(Find); + end; +end; + +{ A new or empty directory, or an IronRuby installed there before: the uninstaller removes + Src, Samples and License from it whole. And a warning for a path with a space in it. } +function NextButtonClick(CurPageID: Integer): Boolean; +var + Dir: String; +begin + Result := True; + if CurPageID = wpSelectDir then + begin + Dir := WizardDirValue; + if DirExists(Dir) and not DirIsEmpty(Dir) and not FileExists(AddBackslash(Dir) + 'ironruby.cmd') then + begin + SuppressibleMsgBox(Dir + ' already holds other files. Choose a new or empty directory for IronRuby.', + mbError, MB_OK, IDOK); + Result := False; + end + else if Pos(' ', Dir) > 0 then + Result := SuppressibleMsgBox(Dir + ' has a space in it. Bundler (its RUBYOPT), the Rails templates and mspec ' + + 'do not work from such a path. Install there anyway?', mbConfirmation, MB_YESNO, IDYES) = IDYES; + end; +end; + +const + RbKey = 'Software\Classes\.rb'; + ProgIdKey = 'Software\Classes\IronRuby.File'; + +{ .rb to IronRuby.File. The program .rb had, such as RubyInstaller's RubyFile, is kept in + IronRuby.File's PreviousProgId, for the uninstaller to give .rb back to. } +procedure AssociateRb; +var + Previous: String; +begin + if RegQueryStringValue(HKA, RbKey, '', Previous) and (Previous <> '') and (Previous <> 'IronRuby.File') then + RegWriteStringValue(HKA, ProgIdKey, 'PreviousProgId', Previous); + RegWriteStringValue(HKA, RbKey, '', 'IronRuby.File'); +end; + +{ Unless another program has taken .rb over since. } +procedure UnassociateRb; +var + Current, Previous: String; +begin + if RegQueryStringValue(HKA, RbKey, '', Current) and (Current = 'IronRuby.File') then + begin + if RegQueryStringValue(HKA, ProgIdKey, 'PreviousProgId', Previous) then + RegWriteStringValue(HKA, RbKey, '', Previous) + else + RegDeleteValue(HKA, RbKey, ''); + end; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + if WizardIsTaskSelected('modifypath') then + AddToPath(ExpandConstant('{app}')); + if WizardIsTaskSelected('assocfiles') then + AssociateRb; + end; +end; + +{ Before the uninstaller deletes IronRuby.File, which holds PreviousProgId. } +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then + begin + RemoveFromPath(ExpandConstant('{app}')); + UnassociateRb; + end; +end; diff --git a/Util/installer/test-installer.ps1 b/Util/installer/test-installer.ps1 new file mode 100644 index 000000000..17e840a66 --- /dev/null +++ b/Util/installer/test-installer.ps1 @@ -0,0 +1,112 @@ +# Installs IronRuby with the installer build-installer.sh made, checks it from a fresh +# environment, as a new terminal would see it, and uninstalls it again: +# +# powershell -File Util\installer\test-installer.ps1 dist\ironruby-4.0.2-win-x64-setup.exe +# powershell -File Util\installer\test-installer.ps1 SETUP.exe -AllUsers # as admin +# +# It installs silently into the default directory, C:\IronRuby40-x64, which must not +# exist yet. Then the install directory must be first on the user's PATH (the system's +# with -AllUsers), and from there `ironruby -v`, `ir`, `irb` and `igem install rack` must +# work. The uninstaller must remove the directory, gems included, and leave PATH exactly +# as it was before. Needs rubygems.org for rack. +param( + [Parameter(Mandatory = $true)][string]$Installer, + [switch]$AllUsers, + [string]$Dir = "$env:SystemDrive\IronRuby40-x64" +) +$ErrorActionPreference = 'Stop' + +function Fail([string]$message) { + Write-Host "test-installer: FAIL: $message" + exit 1 +} + +if ($AllUsers) { + $scope = 'Machine' + $environmentKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' + $mode = '/ALLUSERS' +} else { + $scope = 'User' + $environmentKey = 'HKCU:\Environment' + $mode = '/CURRENTUSER' +} + +# PATH as the registry holds it, %VARIABLES% unexpanded. +function Get-RawPath { + $key = Get-Item $environmentKey + $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames') +} + +# The PATH a terminal opened now would get. +function Get-FreshPath { + [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') +} + +# Runs a command line in cmd.exe with the fresh PATH, as typed at a new prompt, in the +# user's home: cmd looks in the current directory first, and a checkout has launchers of +# the same names. Without quotes in it: PowerShell 7 and Windows PowerShell pass them on +# to cmd differently. +function Invoke-Fresh([string]$commandLine, [string]$stdin = $null) { + $saved = $env:Path + # Not Stop: Windows PowerShell turns a native command's redirected stderr into errors. + $ErrorActionPreference = 'Continue' + Push-Location $env:USERPROFILE + try { + $env:Path = Get-FreshPath + if ($stdin) { $output = $stdin | cmd /d /c $commandLine 2>&1 } else { $output = cmd /d /c $commandLine 2>&1 } + $code = $LASTEXITCODE + } finally { + $env:Path = $saved + Pop-Location + } + $text = ($output | Out-String).Trim() + Write-Host "> $commandLine" + Write-Host $text + if ($code -ne 0) { Fail "$commandLine exited with $code" } + $text +} + +if (Test-Path $Dir) { Fail "$Dir exists already; uninstall IronRuby from there first" } +$installer = (Resolve-Path $Installer).Path +$log = Join-Path (Split-Path $installer) 'install.log' +$pathBefore = Get-RawPath + +Write-Host "test-installer: installing $installer ($mode)" +$setup = Start-Process -FilePath $installer -Wait -PassThru -ArgumentList @( + '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', $mode, "/LOG=`"$log`"") +if ($setup.ExitCode -ne 0) { Get-Content $log -Tail 30; Fail "setup exited with $($setup.ExitCode)" } + +foreach ($launcher in 'ironruby.cmd', 'ir.cmd', 'irb.cmd', 'igem.cmd', 'unins000.exe') { + if (-not (Test-Path (Join-Path $Dir $launcher))) { Fail "no $launcher in $Dir" } +} +$first = ((Get-RawPath) -split ';')[0] +if ($first -ne $Dir) { Fail "$Dir is not first on the $scope PATH, $first is" } + +$description = Invoke-Fresh 'ironruby -v' +if ($description -notmatch '^ironruby 4\.') { Fail "ironruby -v: $description" } +$where = Invoke-Fresh 'where ironruby' +if (($where -split "`r?`n")[0] -ne "$Dir\ironruby.cmd") { Fail "ironruby is not $Dir\ironruby.cmd" } +$engine = Invoke-Fresh 'ir -e puts(RUBY_ENGINE)' +if ($engine -ne 'ironruby') { Fail "ir: $engine" } +$irb = Invoke-Fresh 'irb' "puts 6 * 7`n" +if ($irb -notmatch '42') { Fail 'irb did not run the line it was given' } +Invoke-Fresh 'igem install rack --no-document' | Out-Null +$gems = Join-Path $Dir 'Src\StdLib\ruby\gems\4.0.0\gems' +if (-not (Get-ChildItem $gems -Filter 'rack-*' -Directory)) { Fail "rack is not in $gems" } +$rack = Invoke-Fresh 'ironruby -rrack -e puts(Rack.release)' +if ($rack -notmatch '^\d') { Fail "require 'rack': $rack" } + +Write-Host 'test-installer: uninstalling' +$uninstall = Start-Process -FilePath (Join-Path $Dir 'unins000.exe') -Wait -PassThru -ArgumentList @( + '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART') +if ($uninstall.ExitCode -ne 0) { Fail "the uninstaller exited with $($uninstall.ExitCode)" } +# The uninstaller runs on from a copy of itself, which removes the directory last. +for ($i = 0; $i -lt 120 -and (Test-Path $Dir); $i++) { Start-Sleep -Seconds 1 } +if (Test-Path $Dir) { + Get-ChildItem $Dir -Recurse | Select-Object -First 20 | ForEach-Object { Write-Host " left: $($_.FullName)" } + Fail "$Dir is still there" +} +$pathAfter = Get-RawPath +if ($pathAfter -ne $pathBefore) { Fail "the $scope PATH changed: was`n $pathBefore`nis`n $pathAfter" } +Write-Host "test-installer: OK: installed, ran, installed rack, and uninstalled without a trace" +exit 0