diff --git a/.github/workflows/release-msi.nu b/.github/workflows/release-msi.nu new file mode 100755 index 000000000..6bb4b525a --- /dev/null +++ b/.github/workflows/release-msi.nu @@ -0,0 +1,62 @@ +#!/usr/bin/env nu + +# Created: 2025/05/21 19:05:20 +# Description: +# A script to build Windows MSI packages for NuShell. Need wix 6.0 to be installed. +# The script will download the specified NuShell release, extract it, and create an MSI package. +# Can be run locally or in GitHub Actions. +# To run this script locally: +# load-env { TARGET: 'x86_64-pc-windows-msvc' REF: '0.103.0' GITHUB_REPOSITORY: 'nushell/nushell' } +# nu .github/workflows/release-msi.nu + +def build-msi [] { + let target = $env.TARGET + # We should read the version from the environment variable first + # As we may build the MSI package for a specific version not the latest one + let version = $env.MSI_VERSION + let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' } + + print $'Building msi package for (ansi g)($target)(ansi reset) with version (ansi g)($version)(ansi reset) from tag (ansi g)($env.REF)(ansi reset)...' + fetch-nu-pkg + # Create extra Windows msi release package if dotnet and wix are available + let installed = [dotnet wix] | all { (which $in | length) > 0 } + if $installed and (wix --version | split row . | first | into int) >= 6 { + + print $'(char nl)Start creating Windows msi package with the following contents...' + cd wix; hr-line + cp nu/README.txt . + ls -f nu/* | print + ./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)' + glob **/*.msi | print + # Workaround for https://github.com/softprops/action-gh-release/issues/280 + let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/') + let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi' + mv $wixRelease $msi + print $'MSI archive: ---> ($msi)'; + # Run only in GitHub Actions + if ($env.GITHUB_ACTIONS? | default false | into bool) { + echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT + } + } +} + +def fetch-nu-pkg [] { + mkdir wix/nu + # See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + gh release download $env.REF --repo nushell/nushell --pattern $'*-($env.TARGET).zip' --dir wix/nu + cd wix/nu + let pkg = ls *.zip | get name.0 + unzip $pkg + rm $pkg + ls | print +} + +# Print a horizontal line marker +def 'hr-line' [ + --blank-line(-b) +] { + print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)' + if $blank_line { char nl } +} + +alias main = build-msi diff --git a/.github/workflows/release-msi.yml b/.github/workflows/release-msi.yml new file mode 100644 index 000000000..67e2926dd --- /dev/null +++ b/.github/workflows/release-msi.yml @@ -0,0 +1,103 @@ +# +# REF: +# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude +# +name: Build Windows MSI + +on: + workflow_dispatch: + inputs: + tag: + required: true + description: 'Tag of nushell/nushell' + release: + description: 'Release Tag of Integrations' + +permissions: + contents: write + packages: write + +defaults: + run: + shell: bash + +jobs: + release: + name: Nu + + strategy: + fail-fast: false + matrix: + target: + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + extra: ['bin'] + + include: + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-11-arm + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Wix Toolset 6 for Windows + shell: pwsh + if: ${{ startsWith(matrix.os, 'windows') }} + run: | + dotnet tool install --global wix --version 6.0.0 + dotnet workload install wix + $wixPath = "$env:USERPROFILE\.dotnet\tools" + echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$wixPath;$env:PATH" + wix --version + + - name: Setup Nushell + uses: hustcer/setup-nu@v3 + with: + version: 0.105.1 + + - name: Release MSI Packages + id: nu + run: nu .github/workflows/release-msi.nu + env: + OS: ${{ matrix.os }} + REF: ${{ inputs.tag }} + TARGET: ${{ matrix.target }} + MSI_VERSION: ${{ inputs.tag }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # REF: https://github.com/marketplace/actions/gh-release + - name: Publish Archive + uses: softprops/action-gh-release@v2.0.5 + with: + tag_name: ${{ inputs.release }} + files: ${{ steps.nu.outputs.msi }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sha256sum: + needs: release + name: Create Sha256sum + runs-on: ubuntu-latest + steps: + - name: Download Release Archives + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh release download ${{ inputs.release }} + --repo ${{ github.repository }} + --pattern '*' + --dir release + - name: Create Checksums + run: cd release && rm -f SHA256SUMS && shasum -a 256 * > ../SHA256SUMS + - name: Publish Checksums + uses: softprops/action-gh-release@v2.0.5 + with: + files: SHA256SUMS + tag_name: ${{ inputs.release }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Justfile b/Justfile index f7eaffb78..fdf41d57d 100644 --- a/Justfile +++ b/Justfile @@ -24,7 +24,7 @@ set positional-arguments := true # Used to handle the path separator issue NU_DISTRO_PATH := parent_directory(justfile()) -NU_DIR := parent_directory(`(which nu).path.0`) +NU_DIR := parent_directory(`$nu.current-exe`) _query_plugin := if os_family() == 'windows' { 'nu_plugin_query.exe' } else { 'nu_plugin_query' } # To pass arguments to a dependency, put the dependency diff --git a/wix/License.rtf b/wix/License.rtf new file mode 100644 index 000000000..9e78b15ae Binary files /dev/null and b/wix/License.rtf differ diff --git a/wix/MSI.md b/wix/MSI.md new file mode 100644 index 000000000..3dff44640 --- /dev/null +++ b/wix/MSI.md @@ -0,0 +1,90 @@ +## 使用说明 + +1. 准备工作 : + + - 确保已安装 Wix Toolset 6.0: `dotnet tool install --global wix --version 6.0.0` + +2. 构建 MSI : + + - 对于 x64 架构: `dotnet build -c Release -p:Platform=x64` + - 对于 ARM64 架构: `dotnet build -c Release -p:Platform=arm64` + +3. 安装选项 : + + - 用户范围安装: `winget install Nushell.Nushell --scope user` + - 机器范围安装: `winget install Nushell.Nushell --scope machine` (需要管理员权限) + + # For Per-User Installation + `msiexec /i bin\x64\Release\nu-x64.msi MSIINSTALLPERUSER=1` + + # For Per-Machine Installation (Requires Admin Privileges) + `msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1` + + # MSI Install with Logs + `msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1 /l*v log.txt` + +## 特性说明 + +1. 双重安装范围 :支持用户和机器范围安装 +2. `PATH` 环境变量 :自动将安装目录添加到系统 `PATH` +3. 升级保留 :升级时保留原安装路径 +4. 多架构支持 :支持 `x86_64` 和 `ARM64` 架构 +5. 系统兼容性 :兼容 Windows 7/10/11 + +## User-Facing Changes + +- Nushell should be possible to be installed via winget with both user and machine scope and The default should be user scope + - User scope install by winget: `winget install Nushell.Nushell` + - User scope install by msiexec: `msiexec /i nu-0.104.1-x86_64-pc-windows-msvc.msi /quiet /qn` + - Machine scope install by winget: `winget install Nushell.Nushell --override 'ALLUSERS=1'` + - Machine scope install by msiexec: `msiexec /i nu-0.104.1-x86_64-pc-windows-msvc.msi ALLUSERS=1` + - Note that `--scope` flag for `winget install` does not work use `--override` instead + - Default user scope install dir: `$'($nu.home-path)\AppData\Local\Programs\nu\'` + - Default machine scope install dir: `C:\Program Files\nu\` +- When a standard user runs the installer and selects "Install for everyone" (per-machine installation), Windows will automatically trigger a UAC prompt, the user can enter admin credentials and the installation will proceed with elevated privileges + +## Test Case + +- 为当前用户安装 Nushell: + - 检查默认安装路径是否正确 + - 检查静默安装: `msiexec /i $pkg MSIINSTALLPERUSER=1 /quiet /qn` 是否正常 + - 检查静默安装: `winget install --manifest manifests\n\Nushell\Nushell\0.104.1 --scope user` 是否正常 + - 安装过程中是否不会出现 UAC prompt + - 检查安装完成后环境变量是否正确添加 + - 检查注册表变量是否正确设置 + - 检查 Window Terminal 配置文件是否添加 + - 如果没有选择 Window Terminal 配置 Feature 则不会被安装 + - 如果是升级安装是否保持原来的安装路径 + - 卸载 Nushell 并检查文件/环境变量/注册表/Windows Terminal 配置文件是否被清理掉 + +- 为所有用户安装 Nushell + - 如果是普通用户安装过程中是否会出现 UAC prompt + - 检查默认安装路径是否正确 + - 检查静默安装: `winget install --manifest manifests\n\Nushell\Nushell\0.104.1 --scope machine` 是否正常 + - 检查是否允许用户选择自定义安装路径 + - 选择自定义安装路径是否能成功安装 + - 如果是升级安装是否保持原来的安装路径 + - 检查安装完成后环境变量是否正确添加 + - 检查注册表变量是否正确设置 + - 检查 Window Terminal 配置文件是否添加 + - 如果没有选择 Window Terminal 配置 Feature 则不会被安装 + - 卸载 Nushell 并检查文件/环境变量/注册表/Windows Terminal 配置文件是否被清理掉 + +## 当前已知问题 + +- [x] 卸载的时候环境变量没有被清理掉; +- 支持通过 INSTALLDIR 属性指定安装路径 +- `winget install --scope machine` 不支持 +- [x] `winget install --override 'ALLUSERS=1'` 可以正常在全局范围安装应用 +- [x] `msiexec /i $pkg MSIINSTALLPERUSER=1 /quiet /qn` 静默安装路径异常 +- [x] 为所有用户安装的时候没有正确安装 WindowsTerminalProfileFeature +- [x] 为所有用户安装的时候默认安装路径是 C:\Program Files (x86)\nu 而不是 C:\Program Files\nu; +- [x] 安装 Scope 选择切换到为所有用户安装时默认选中的路径是 C:\Program Files (x86)\nu 而不是 C:\Program Files\nu; + +## REF + +- https://docs.firegiant.com/quick-start/ +- https://docs.firegiant.com/wix/schema/wxs/package/ +- https://learn.microsoft.com/en-ca/windows/win32/msi/formatted +- https://learn.microsoft.com/en-us/windows/win32/msi/single-package-authoring +- https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec diff --git a/wix/README.txt b/wix/README.txt new file mode 100644 index 000000000..e69de29bb diff --git a/wix/main.wixproj b/wix/main.wixproj new file mode 100644 index 000000000..da43a12bc --- /dev/null +++ b/wix/main.wixproj @@ -0,0 +1,36 @@ + + + Package + nu-$(Platform) + 82D756D2-19FA-4F09-B10F-64942E89F364 + + SourceDir=$(MSBuildProjectDirectory)\nu; + + true + ICE80 + + + + x64 + $(DefineConstants) + + + + arm64 + $(DefineConstants) + + + + + + + + + + + + + + + + diff --git a/wix/main.wxs b/wix/main.wxs new file mode 100644 index 000000000..bb631dd58 --- /dev/null +++ b/wix/main.wxs @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wix/nu.ico b/wix/nu.ico new file mode 100644 index 000000000..b74f2a336 Binary files /dev/null and b/wix/nu.ico differ diff --git a/wix/prepare-winget.nu b/wix/prepare-winget.nu new file mode 100644 index 000000000..579d36430 --- /dev/null +++ b/wix/prepare-winget.nu @@ -0,0 +1,57 @@ +const FROM_REF = '0.105.1' +const PREV_VER = '0.105.0' +const LAST_VER = '0.105.1' +const OSS_DEST = 'oss://terminus-new-trantor/open-tools/misc' +const OSS_ENDPOINT = 'https://terminus-new-trantor.oss-cn-hangzhou.aliyuncs.com/open-tools/misc' + +export def prepare [] { + + rm -rf obj/ bin/ + nu -c $'NU_RELEASE_VERSION=($PREV_VER) dotnet build -c Release -p:Platform=x64' + ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($PREV_VER)-x86_64-pc-windows-msvc.msi' + + rm -rf obj/ bin/ + nu -c $'NU_RELEASE_VERSION=($LAST_VER) dotnet build -c Release -p:Platform=x64' + ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi' +} + +export def upgrade-nu-by-winget [ + --no-hash(-n), +] { + + let prevUrl = $'($OSS_ENDPOINT)/nu-($PREV_VER)-x86_64-pc-windows-msvc.msi' + let lastUrl = $'($OSS_ENDPOINT)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi' + # sudo winget settings --enable InstallerHashOverride + winget uninstall nushell | complete + if ($no_hash) { + winget install --manifest manifests\n\Nushell\Nushell\($PREV_VER) --ignore-security-hash --silent + winget upgrade --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent + return + } + komac update Nushell.Nushell --dry-run -v $PREV_VER -u $prevUrl --output . + komac update Nushell.Nushell --dry-run -v $LAST_VER -u $lastUrl --output . + winget install --manifest manifests\n\Nushell\Nushell\($PREV_VER) --ignore-security-hash --silent + winget upgrade --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent +} + +def rebuild [--fetch(-f)] { + if $fetch { fetch-nu-pkg } + rm -rf obj/ bin/ + nu -c $'NU_RELEASE_VERSION=($LAST_VER) dotnet build -c Release -p:Platform=x64' + ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi' + winget uninstall nushell | complete + # msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1 + winget install --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent --scope machine +} + +def fetch-nu-pkg [] { + mkdir nu + gh release download $FROM_REF --repo nushell/nushell --pattern $'*-x86_64-*.zip' --dir nu + cd nu + let pkg = ls *.zip | get name.0 + unzip $pkg + rm $pkg + ls | print +} + +alias main = rebuild diff --git a/wix/windows-terminal-profile.json b/wix/windows-terminal-profile.json new file mode 100644 index 000000000..59866bb52 --- /dev/null +++ b/wix/windows-terminal-profile.json @@ -0,0 +1,11 @@ +{ + "profiles": [ + { + "guid": "{47302f9c-1ac4-566c-aa3e-8cf29889d6ab}", + "name": "Nushell", + "commandline": "nu.exe", + "icon": "nu.ico", + "startingDirectory": "%USERPROFILE%" + } + ] +}