diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 3fbf8e1d..00000000 --- a/.gitattributes +++ /dev/null @@ -1,9 +0,0 @@ -* text=auto eol=lf - -*.exe binary -*.dll binary -*.bin binary -*.cer binary - -# Cross-language verify-cache fixtures: payload bytes must match Go canonicalPayload exactly. -shim/testdata/verifycache/payload.txt text eol=lf diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..bcaab3b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,43 @@ +If this is a question about how to use NVM4W, please use [stackoverflow](http://stackoverflow.com/questions/tagged/nvm-windows) instead. + +If this is an issue regarding antivirus, make sure you search the existing issues first. + +### My Environment + +- [ ] Windows 7 or below (not truly supported due to EOL - see wiki for details) +- [ ] Windows 8 +- [ ] Windows 8.1 +- [ ] Windows 10 +- [ ] Windows 10 IoT Core +- [ ] Windows Server 2012 +- [ ] Windows Server 2012 R2 +- [ ] Windows Server 2016 + +- [ ] My Windows installation is non-English. + +### I have already... + +- [ ] read the README to be aware of npm gotchas & antivirus issues. +- [ ] reviewed the [wiki](https://github.com/coreybutler/nvm-windows/wiki) to make sure my issue hasn't already been resolved. +- [ ] verified I'm using an account with administrative privileges. +- [ ] searched the issues (open and closed) to make sure this isn't a duplicate. +- [ ] made sure this isn't a question about how to use NVM for Windows, since [gitter](https://gitter.im/coreybutler/nvm-windows?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) is used for questions and comments. + +### My issue is related to (check only those which apply): + +- [ ] settings.txt +- [ ] proxy support +- [ ] 32 or 64 bit support + +### Expected Behavior + +Fill me in... + +### Actual Behavior + +Fill me in... + +### Steps to reproduce the problem: + +Fill me in... + diff --git a/.github/workflows/installer-report.yml b/.github/workflows/installer-report.yml deleted file mode 100644 index 4322ee9b..00000000 --- a/.github/workflows/installer-report.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Installer report - -on: - issues: - types: [opened] - -permissions: - issues: write - -jobs: - label-and-dedupe: - runs-on: ubuntu-latest - steps: - - name: Label autosubmission and comment on the existing install code - uses: actions/github-script@v7 - with: - script: | - const issue = context.payload.issue - const body = issue.body || "" - if (!body.includes("autosubmission")) return - const match = body.match(/Install code:\s*(NVM41\d\d)/) - if (!match) return - const code = match[1] - const owner = context.repo.owner - const repo = context.repo.repo - async function ensureLabel(name) { - try { - await github.rest.issues.createLabel({ - owner, - repo, - name, - color: "b60205", - description: "Installer report" - }) - } catch (error) { - if (error.status !== 422) throw error - } - } - await ensureLabel("autosubmission") - await ensureLabel(code) - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: issue.number, - labels: ["autosubmission", code] - }) - const listed = await github.rest.issues.listForRepo({ - owner, - repo, - state: "open", - labels: code, - per_page: 20 - }) - const older = listed.data - .filter((item) => item.number !== issue.number) - .sort((a, b) => a.number - b.number) - if (older.length === 0) return - const target = older[0] - await github.rest.issues.createComment({ - owner, - repo, - issue_number: target.number, - body: `Another setup report arrived in #${issue.number}.\n\n${body}` - }) - await github.rest.issues.createComment({ - owner, - repo, - issue_number: issue.number, - body: `This report was added to #${target.number}.` - }) - await github.rest.issues.update({ - owner, - repo, - issue_number: issue.number, - state: "closed", - state_reason: "duplicate" - }) diff --git a/.github/workflows/readme-release-badges.yml b/.github/workflows/readme-release-badges.yml deleted file mode 100644 index 0b81e2fa..00000000 --- a/.github/workflows/readme-release-badges.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Update release docs - -# Release Community Build publishes with github.token, which does not emit release -# events to other workflows. workflow_run is the primary trigger after publish. -on: - release: - types: [published, released, prereleased] - workflow_run: - workflows: ["Release Community Build"] - types: [completed] - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: release-docs - cancel-in-progress: true - -jobs: - update: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' - runs-on: windows-latest - defaults: - run: - shell: pwsh - steps: - - name: Resolve target branch - id: branch - env: - GH_TOKEN: ${{ github.token }} - run: | - if ("${{ github.event_name }}" -eq "workflow_run") { - $name = gh repo view "${{ github.repository }}" --json defaultBranchRef -q .defaultBranchRef.name - } - elseif ("${{ github.event_name }}" -eq "release") { - $name = gh repo view "${{ github.repository }}" --json defaultBranchRef -q .defaultBranchRef.name - } - else { - $name = "${{ github.ref_name }}" - if ([string]::IsNullOrWhiteSpace($name)) { - $name = gh repo view "${{ github.repository }}" --json defaultBranchRef -q .defaultBranchRef.name - } - } - if ([string]::IsNullOrWhiteSpace($name)) { - throw "Could not resolve default branch." - } - Write-Host "Target branch -> $name" - "name=$name" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - - name: Checkout - uses: actions/checkout@v7 - with: - ref: ${{ steps.branch.outputs.name }} - token: ${{ github.token }} - - - name: Update README badges - env: - GH_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - & .\build\steps\Update-ReadmeReleaseBadges.ps1 - - - name: Update THANKS.md contributors - env: - GH_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - & .\build\steps\Update-ThanksContributors.ps1 - - - name: Commit and push - env: - GH_TOKEN: ${{ github.token }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git add README.md THANKS.md - $status = git status --porcelain README.md THANKS.md - if ([string]::IsNullOrWhiteSpace($status)) { - Write-Host "No README/THANKS changes to commit." - exit 0 - } - - git commit -m "chore: update release docs (badges + contributors)" - git push origin HEAD diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e3ed69f8..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,595 +0,0 @@ -name: Release Community Build - -on: - workflow_dispatch: - inputs: - architecture: - description: Target architecture(s) β€” both land on the same GitHub Release - required: true - default: both - type: choice - options: - - both - - amd64 - - arm64 - publish_release: - description: Create/update GitHub Release from CLI manifest version - required: false - default: true - type: boolean - override_existing_release: - description: Replace existing setup.exe + sync.exe assets on that release - required: false - default: false - type: boolean - release_kind: - description: Release Type - required: true - default: General - type: choice - options: - - General - - alpha - - beta - - rc - - hotfix - hotfix_base: - description: "Hotfix Mainline - specify general release to apply hotfix to - ex: 2.0.1" - required: false - default: "" - type: string - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - id-token: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - QGO_VERSION: "1.2.8" - INNO_SETUP_VERSION: "6.7.1" - ARTIFACT_SIGNING_CLIENT_VERSION: "1.0.95" - ARTIFACT_SIGNING_TOOLS_DIR: ${{ github.workspace }}\.tools\artifact-signing - -jobs: - prepare: - name: Version / release gate - runs-on: windows-latest - defaults: - run: - shell: pwsh - outputs: - version: ${{ steps.gate.outputs.version }} - tag: ${{ steps.gate.outputs.tag }} - matrix: ${{ steps.matrix.outputs.matrix }} - arches: ${{ steps.matrix.outputs.arches }} - steps: - - name: Check GitHub App secrets - env: - CLIENT_ID: ${{ secrets.GH_APP_CLIENT_ID }} - PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} - run: | - if ([string]::IsNullOrWhiteSpace($env:CLIENT_ID)) { throw "Missing secrets.GH_APP_CLIENT_ID" } - if ([string]::IsNullOrWhiteSpace($env:PRIVATE_KEY)) { throw "Missing secrets.GH_APP_PRIVATE_KEY (PEM)." } - - - name: Create GitHub App token - id: app-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ secrets.GH_APP_CLIENT_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - permission-contents: read - permission-metadata: read - - - name: Checkout - uses: actions/checkout@v7 - with: - token: ${{ steps.app-token.outputs.token }} - submodules: false - - - name: Checkout cli submodule (manifest version) - env: - APP_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - $owner = "${{ github.repository_owner }}" - $token = $env:APP_TOKEN - $url = "https://x-access-token:${token}@github.com/${owner}/cli.git" - git config -f .gitmodules "submodule.cli.url" $url - git config "submodule.cli.url" $url - git submodule sync --recursive - if ($LASTEXITCODE -ne 0) { throw "git submodule sync failed" } - git -c protocol.version=2 submodule update --init --force --depth=1 cli - if ($LASTEXITCODE -ne 0) { throw "git submodule update cli failed with exit code $LASTEXITCODE" } - - - name: Build architecture matrix - id: matrix - env: - ARCH_INPUT: ${{ inputs.architecture }} - run: | - if ($env:ARCH_INPUT -eq "both") { - $arches = @("amd64", "arm64") - $matrixJson = '{"architecture":["amd64","arm64"]}' - } - else { - $arches = @($env:ARCH_INPUT) - $matrixJson = "{`"architecture`":[`"$($env:ARCH_INPUT)`"]}" - } - "matrix=$matrixJson" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "arches=$($arches -join ',')" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - Write-Host "matrix -> $matrixJson" - - - name: Resolve version / release gate - id: gate - env: - GH_TOKEN: ${{ github.token }} - PUBLISH_RELEASE: ${{ inputs.publish_release }} - OVERRIDE_EXISTING_RELEASE: ${{ inputs.override_existing_release }} - ARCHES: ${{ steps.matrix.outputs.arches }} - RELEASE_KIND: ${{ inputs.release_kind }} - HOTFIX_BASE: ${{ inputs.hotfix_base }} - run: | - . .\build\common.ps1 - $null = Initialize-NvmBuildContext -SkipZig - # Always read base from on-disk manifest (ignore any prior NVM_CLI_VERSION). - $manifestPath = Get-NvmCliManifestPath - $baseVersion = [string](Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json).version - if ([string]::IsNullOrWhiteSpace($baseVersion)) { - throw "CLI manifest does not define version: $manifestPath" - } - $kind = if ([string]::IsNullOrWhiteSpace($env:RELEASE_KIND)) { "none" } else { $env:RELEASE_KIND.Trim().ToLowerInvariant() } - if ($kind -eq "general") { $kind = "none" } - $hotfixBase = if ($null -eq $env:HOTFIX_BASE) { "" } else { $env:HOTFIX_BASE.Trim() } - if ($kind -eq "hotfix" -and [string]::IsNullOrWhiteSpace($hotfixBase)) { - $hotfixBase = Get-NvmLatestStableReleaseVersion - Write-Host ("Hotfix base -> {0} (latest non-prerelease release)" -f $hotfixBase) - } - elseif ($kind -ne "hotfix" -and -not [string]::IsNullOrWhiteSpace($hotfixBase)) { - Write-Host ("Ignoring hotfix base '{0}' because Release Type is {1}." -f $hotfixBase, $kind) - $hotfixBase = "" - } - $effective = Resolve-NvmReleaseVersion -ManifestVersion $baseVersion -Kind $kind -HotfixBase $hotfixBase - if ($effective -ne $baseVersion) { - Write-Host ("Prerelease stamp -> {0} (manifest {1})" -f $effective, $baseVersion) - } - else { - Write-Host ("Release version -> {0} (no prerelease)" -f $effective) - } - [Environment]::SetEnvironmentVariable("NVM_CLI_VERSION", $effective, "Process") - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { - Add-Content -LiteralPath $env:GITHUB_ENV -Value "NVM_CLI_VERSION=$effective" - } - - $archList = @($env:ARCHES -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $gateArgs = @{ - Mode = "Gate" - Architectures = $archList - Enabled = ($env:PUBLISH_RELEASE -eq "true") - Version = $effective - } - if ($env:OVERRIDE_EXISTING_RELEASE -eq "true") { - $gateArgs["OverrideExisting"] = $true - } - & .\build\steps\Publish-GitHubRelease.ps1 @gateArgs - - build: - name: Build (${{ matrix.architecture }}) - needs: prepare - runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-latest' }} - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} - defaults: - run: - shell: pwsh - env: - GOBIN: ${{ github.workspace }}\go-bin - ARCHITECTURE: ${{ matrix.architecture }} - NVM_CLI_VERSION: ${{ needs.prepare.outputs.version }} - - steps: - - name: Check GitHub App secrets - env: - CLIENT_ID: ${{ secrets.GH_APP_CLIENT_ID }} - PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} - run: | - if ([string]::IsNullOrWhiteSpace($env:CLIENT_ID)) { throw "Missing secrets.GH_APP_CLIENT_ID" } - if ([string]::IsNullOrWhiteSpace($env:PRIVATE_KEY)) { throw "Missing secrets.GH_APP_PRIVATE_KEY (PEM)." } - - - name: Create GitHub App token - id: app-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ secrets.GH_APP_CLIENT_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - permission-contents: read - permission-metadata: read - - - name: Verify App can read submodule repos - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - $owner = "${{ github.repository_owner }}" - $missing = New-Object System.Collections.Generic.List[string] - foreach ($repo in @("nvm", "cli", "common", "shim", "sync")) { - Write-Host "GET repos/$owner/$repo" - gh api "repos/$owner/$repo" --jq .full_name - if ($LASTEXITCODE -ne 0) { - $missing.Add("$owner/$repo") | Out-Null - } - } - if ($missing.Count -gt 0) { - Write-Host "App installation cannot read: $($missing -join ', ')" - Write-Host "Fix: GitHub App install settings for org '$owner'" - Write-Host " - Repository access: All repositories, OR" - Write-Host " - Only select repositories: add nvm, cli, common, shim, sync" - Write-Host " - Permissions: Contents Read" - throw "App missing repo access: $($missing -join ', ')" - } - - - name: Checkout - uses: actions/checkout@v7 - with: - token: ${{ steps.app-token.outputs.token }} - submodules: false - - - name: Checkout submodules - env: - APP_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - $owner = "${{ github.repository_owner }}" - $token = $env:APP_TOKEN - if ([string]::IsNullOrWhiteSpace($token)) { - throw "APP_TOKEN empty" - } - - foreach ($name in @("cli", "common", "shim", "sync")) { - $url = "https://x-access-token:${token}@github.com/${owner}/${name}.git" - git config -f .gitmodules "submodule.${name}.url" $url - if ($LASTEXITCODE -ne 0) { throw "git config .gitmodules submodule.$name.url failed" } - git config "submodule.${name}.url" $url - if ($LASTEXITCODE -ne 0) { throw "git config submodule.$name.url failed" } - Write-Host "submodule.$name.url -> https://x-access-token:***@github.com/${owner}/${name}.git" - } - - git submodule sync --recursive - if ($LASTEXITCODE -ne 0) { throw "git submodule sync failed" } - - git -c protocol.version=2 submodule update --init --force --depth=1 --recursive - if ($LASTEXITCODE -ne 0) { - throw "git submodule update failed with exit code $LASTEXITCODE" - } - - - name: Stamp CLI manifest version (prerelease / gate) - env: - EFFECTIVE_VERSION: ${{ needs.prepare.outputs.version }} - run: | - if ([string]::IsNullOrWhiteSpace($env:EFFECTIVE_VERSION)) { - throw "prepare.outputs.version empty (cannot stamp cli/src/manifest.json)" - } - . .\build\common.ps1 - $null = Initialize-NvmBuildContext -SkipZig - $base = (Get-Content -LiteralPath (Get-NvmCliManifestPath) -Raw -Encoding UTF8 | ConvertFrom-Json).version - Set-NvmCliManifestVersion -Version $env:EFFECTIVE_VERSION - if ($env:EFFECTIVE_VERSION -ne [string]$base) { - Write-Host ("Prerelease stamp -> {0} (base {1})" -f $env:EFFECTIVE_VERSION, $base) - } - - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: cli/src/go.mod - cache: true - cache-dependency-path: | - cli/src/go.sum - sync/src/go.sum - - - name: Cache qgo - uses: actions/cache@v6 - with: - path: ${{ env.GOBIN }} - key: qgo-${{ runner.os }}-${{ runner.arch }}-${{ env.QGO_VERSION }} - - - name: Install qgo - run: | - New-Item -ItemType Directory -Force -Path $env:GOBIN | Out-Null - if (!(Test-Path "$env:GOBIN\qgo.exe")) { - go install github.com/quikdev/go/cmd/qgo@v$env:QGO_VERSION - } else { - Write-Host "using cached qgo binary" - } - "$env:GOBIN" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Install go-winres - run: | - $cmd = Get-Command go-winres -ErrorAction SilentlyContinue - if ($null -ne $cmd) { - Write-Host "go-winres already on PATH -> $($cmd.Source)" - } - else { - Write-Host "Installing go-winres into GOBIN ($env:GOBIN)..." - New-Item -ItemType Directory -Force -Path $env:GOBIN | Out-Null - go install github.com/tc-hib/go-winres@latest - if ($LASTEXITCODE -ne 0) { throw "go install go-winres failed with exit code $LASTEXITCODE" } - } - if (-not (Test-Path (Join-Path $env:GOBIN "go-winres.exe"))) { - throw "go-winres.exe missing under GOBIN after install" - } - Write-Host "go-winres -> $(Join-Path $env:GOBIN 'go-winres.exe')" - - - name: Resolve Zig version - id: zig-ver - run: | - $candidates = @( - (Join-Path $env:GITHUB_WORKSPACE "shim\.zigversion"), - (Join-Path $env:GITHUB_WORKSPACE "build\.zigversion") - ) - $path = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 - if (-not $path) { - throw "No Zig version file found (tried shim/.zigversion, build/.zigversion)" - } - $version = (Get-Content -LiteralPath $path -Raw).Trim() - if ([string]::IsNullOrWhiteSpace($version)) { - throw "Zig version file is empty: $path" - } - "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - "source=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - Write-Host "Zig version $version (from $path)" - - - name: Set up Zig - id: setup-zig - uses: xyzzylabs/setup-zig@v1 - with: - version: ${{ steps.zig-ver.outputs.version }} - use-cache: true - cache-key: shim-${{ steps.zig-ver.outputs.version }} - - - name: Zig version - run: | - Write-Host "zig version (resolved) = ${{ steps.setup-zig.outputs.zig-version }}" - zig version - - - name: Install Inno Setup - run: | - $ver = $env:INNO_SETUP_VERSION - $isccCandidates = @( - "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", - "$env:ProgramFiles\Inno Setup 6\ISCC.exe", - "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" - ) - $existing = $isccCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 - if ($existing) { - Write-Host "ISCC already present -> $existing" - return - } - $exe = Join-Path $env:RUNNER_TEMP "innosetup-$ver.exe" - $url = "https://files.jrsoftware.org/is/6/innosetup-$ver.exe" - Write-Host "Downloading $url" - Invoke-WebRequest -Uri $url -OutFile $exe - Start-Process -FilePath $exe -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART","/SP-" -Wait -NoNewWindow - $iscc = $isccCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 - if (-not $iscc) { - throw "Inno Setup $ver install did not produce ISCC.exe" - } - Write-Host "ISCC -> $iscc" - - - name: Build community payload (no installer yet) - run: | - & .\build\main.ps1 -Architecture $env:ARCHITECTURE -SkipInstaller - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "build\main.ps1 failed with exit code $LASTEXITCODE" - } - - - name: Check Artifact Signing secrets - env: - ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }} - ARTIFACT_SIGNING_ACCOUNT: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT }} - ARTIFACT_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.ARTIFACT_SIGNING_CERTIFICATE_PROFILE }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - run: | - $missing = New-Object System.Collections.Generic.List[string] - foreach ($item in @( - @{ Name = "ARTIFACT_SIGNING_ENDPOINT"; Value = $env:ARTIFACT_SIGNING_ENDPOINT }, - @{ Name = "ARTIFACT_SIGNING_ACCOUNT"; Value = $env:ARTIFACT_SIGNING_ACCOUNT }, - @{ Name = "ARTIFACT_SIGNING_CERTIFICATE_PROFILE"; Value = $env:ARTIFACT_SIGNING_CERTIFICATE_PROFILE }, - @{ Name = "AZURE_CLIENT_ID"; Value = $env:AZURE_CLIENT_ID }, - @{ Name = "AZURE_TENANT_ID"; Value = $env:AZURE_TENANT_ID }, - @{ Name = "AZURE_SUBSCRIPTION_ID"; Value = $env:AZURE_SUBSCRIPTION_ID } - )) { - if ([string]::IsNullOrWhiteSpace([string]$item.Value)) { $missing.Add($item.Name) } - } - if ($missing.Count -gt 0) { - throw ("Missing Actions secrets for community Authenticode: {0}" -f ($missing -join ", ")) - } - - - name: Cache Artifact Signing client - uses: actions/cache@v6 - with: - path: ${{ env.ARTIFACT_SIGNING_TOOLS_DIR }} - key: artifact-signing-client-${{ env.ARTIFACT_SIGNING_CLIENT_VERSION }}-${{ runner.os }}-${{ runner.arch }} - - - name: Azure login (Artifact Signing) - uses: azure/login@v3 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Sign payload executables - env: - ARTIFACT_SIGNING_ENABLED: "true" - ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }} - ARTIFACT_SIGNING_ACCOUNT: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT }} - ARTIFACT_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.ARTIFACT_SIGNING_CERTIFICATE_PROFILE }} - ARTIFACT_SIGNING_AUTH: azure-cli - ARTIFACT_SIGNING_TOOLS_DIR: ${{ env.ARTIFACT_SIGNING_TOOLS_DIR }} - ARTIFACT_SIGNING_CLIENT_VERSION: ${{ env.ARTIFACT_SIGNING_CLIENT_VERSION }} - run: | - & .\build\steps\Sign-CommunityArtifacts.ps1 -Architecture $env:ARCHITECTURE - - - name: Build Inno Setup (embeds signed payload) - run: | - & .\build\steps\Build-Installer.ps1 -Architecture $env:ARCHITECTURE - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "Build-Installer.ps1 failed with exit code $LASTEXITCODE" - } - - - name: Sign installer + sync release assets - env: - ARTIFACT_SIGNING_ENABLED: "true" - ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }} - ARTIFACT_SIGNING_ACCOUNT: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT }} - ARTIFACT_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.ARTIFACT_SIGNING_CERTIFICATE_PROFILE }} - ARTIFACT_SIGNING_AUTH: azure-cli - ARTIFACT_SIGNING_TOOLS_DIR: ${{ env.ARTIFACT_SIGNING_TOOLS_DIR }} - ARTIFACT_SIGNING_CLIENT_VERSION: ${{ env.ARTIFACT_SIGNING_CLIENT_VERSION }} - run: | - & .\build\steps\Sign-CommunityArtifacts.ps1 -Architecture $env:ARCHITECTURE -InstallerOnly - - - name: Verify Authenticode on release assets - run: | - . .\build\common.ps1 - $binRoot = Join-Path $env:GITHUB_WORKSPACE "bin" - $ctx = Initialize-NvmBuildContext -BinRoot $binRoot - $setup = Get-NvmInstallerSetupPath -Version $ctx.CliVersion -Architecture $env:ARCHITECTURE - $syncAsset = Get-NvmSyncReleaseAssetPath -Version $ctx.CliVersion -Architecture $env:ARCHITECTURE - foreach ($path in @($setup, $syncAsset) + @(Get-NvmExpectedExePaths -BinRoot $binRoot -Component All)) { - $sig = Get-AuthenticodeSignature -LiteralPath $path - if ($sig.Status -ne "Valid") { - throw ("Authenticode invalid for {0}: {1} ({2})" -f $path, $sig.Status, $sig.StatusMessage) - } - Write-Host ("OK {0} -> {1}" -f $path, $sig.SignerCertificate.Subject) - } - foreach ($path in (Get-NvmExpectedEventProviderPaths -BinRoot $binRoot)) { - if ([System.IO.Path]::GetExtension($path) -ine ".dll") { continue } - $sig = Get-AuthenticodeSignature -LiteralPath $path - if ($sig.Status -ne "Valid") { - throw ("Authenticode invalid for {0}: {1} ({2})" -f $path, $sig.Status, $sig.StatusMessage) - } - Write-Host ("OK {0} -> {1}" -f $path, $sig.SignerCertificate.Subject) - } - - - name: Verify outputs - run: | - . .\build\common.ps1 - $binRoot = Join-Path $env:GITHUB_WORKSPACE "bin" - $ctx = Initialize-NvmBuildContext -BinRoot $binRoot - $expected = Get-NvmExpectedExePaths -BinRoot $binRoot -Component All - $setup = Get-NvmInstallerSetupPath -Version $ctx.CliVersion -Architecture $env:ARCHITECTURE - $syncAsset = Get-NvmSyncReleaseAssetPath -Version $ctx.CliVersion -Architecture $env:ARCHITECTURE - - if (-not (Test-Path -LiteralPath $syncAsset -PathType Leaf)) { - throw "Missing staged sync release asset: $syncAsset" - } - - $eventAssets = Get-NvmExpectedEventProviderPaths -BinRoot $binRoot - foreach ($path in $eventAssets) { - Assert-NvmFile -Path $path -Label ([System.IO.Path]::GetFileName($path)) | Out-Null - } - - Write-NvmBuildJobSummaryHeader ` - -Version $ctx.CliVersion ` - -Architecture $env:ARCHITECTURE ` - -ZigVersion ${{ steps.setup-zig.outputs.zig-version }} - - Write-NvmPayloadSummary -Paths $expected -Title "Executables" -DisplayRoot $binRoot - Write-NvmPayloadSummary -Paths $eventAssets -Title "Event provider" -DisplayRoot $binRoot - Write-NvmPayloadSummary -Paths @($setup, $syncAsset) -Title "Release assets" -DisplayRoot $ctx.RepoRoot - - - name: Upload release artifacts - uses: actions/upload-artifact@v7 - with: - name: installer-${{ env.ARCHITECTURE }} - include-hidden-files: true - path: | - .dist/*-setup.exe - .dist/*-sync.exe - if-no-files-found: error - retention-days: 30 - - release: - name: Publish GitHub Release - needs: [prepare, build] - if: success() && inputs.publish_release == true - runs-on: windows-latest - defaults: - run: - shell: pwsh - steps: - - name: Checkout (release scripts) - uses: actions/checkout@v7 - with: - sparse-checkout: | - build - sparse-checkout-cone-mode: true - - - name: Download build artifacts - uses: actions/download-artifact@v8 - with: - path: release-assets - pattern: installer-* - merge-multiple: false - - - name: Publish GitHub Release - env: - GH_TOKEN: ${{ github.token }} - OVERRIDE_EXISTING_RELEASE: ${{ inputs.override_existing_release }} - RELEASE_VERSION: ${{ needs.prepare.outputs.version }} - ARCHES: ${{ needs.prepare.outputs.arches }} - run: | - $archList = @($env:ARCHES -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - if ($archList.Count -eq 0) { - throw "prepare.outputs.arches empty" - } - if ([string]::IsNullOrWhiteSpace($env:RELEASE_VERSION)) { - throw "prepare.outputs.version empty" - } - if (-not (Test-Path -LiteralPath "release-assets" -PathType Container)) { - throw "release-assets directory missing after download-artifact" - } - Get-ChildItem -LiteralPath "release-assets" -Recurse -File | ForEach-Object { - Write-Host ("artifact file -> {0}" -f $_.FullName) - } - - $pubArgs = @{ - Mode = "Publish" - Architectures = $archList - Version = $env:RELEASE_VERSION - AssetRoot = (Join-Path $PWD "release-assets") - Enabled = $true - } - if ($env:OVERRIDE_EXISTING_RELEASE -eq "true") { - $pubArgs["OverrideExisting"] = $true - } - & .\build\steps\Publish-GitHubRelease.ps1 @pubArgs - - - name: VirusTotal Scan - id: vt - uses: crazy-max/ghaction-virustotal@v5 - with: - vt_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} - github_token: ${{ github.token }} - update_release_body: false - request_rate: 4 - files: | - release-assets/**/*.exe - - - name: Update VirusTotal section in release notes - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.prepare.outputs.tag }} - run: | - if ([string]::IsNullOrWhiteSpace($env:TAG)) { - throw "prepare.outputs.tag empty" - } - # Always rebuild ## VirusTotal from current artifact SHA256s (covers - # first publish and override_existing_release asset replacement). - & .\build\steps\Update-VirusTotalReleaseNotes.ps1 ` - -Tag $env:TAG ` - -AssetRoot (Join-Path $PWD "release-assets") diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml deleted file mode 100644 index 2c7771d4..00000000 --- a/.github/workflows/winget.yml +++ /dev/null @@ -1,409 +0,0 @@ -name: Publish to WinGet - -on: - workflow_dispatch: - inputs: - release: - description: Which GitHub Release to use β€” latest skips drafts and pre-releases - required: true - type: choice - default: latest - options: - - latest - - custom - release_tag: - description: Release tag when release=custom (e.g. v2.0.0-alpha.2). Required for custom; ignored for latest. - required: false - type: string - default: '' - dry_run: - description: "Dry run (checked/default): validate only, no winget-pkgs PR. Uncheck to publish to WinGet." - required: true - default: true - type: boolean - install_test: - description: "Best-effort silent install smoke test using release asset already on runner (not winget install). Warns and skips on failure; never fails dry-run." - required: true - default: true - type: boolean - verify_public_urls: - description: "Verify anonymous GitHub release URLs download and match SHA256 (required for WinGet)." - required: true - default: true - type: boolean - -permissions: - contents: read - -concurrency: - group: winget-${{ github.repository }}-${{ inputs.release }}-${{ inputs.release_tag }}-${{ inputs.dry_run }} - cancel-in-progress: false - -env: - WINGET_PACKAGE_IDENTIFIER: AuthorSoftware.NVMWindows - -jobs: - validate: - name: Generate and validate manifest - runs-on: windows-latest - defaults: - run: - shell: pwsh - outputs: - tag: ${{ steps.meta.outputs.tag }} - version: ${{ steps.meta.outputs.version }} - manifest_dir: ${{ steps.validate.outputs.manifest_dir }} - pull_request_url: ${{ steps.submit.outputs.pull_request_url }} - steps: - - name: Checkout manifest generator - uses: actions/checkout@v7 - with: - sparse-checkout: | - build/steps/New-WingetManifest.ps1 - LICENSE - sparse-checkout-cone-mode: false - - - name: Resolve release tag - id: meta - env: - GH_TOKEN: ${{ github.token }} - RELEASE_CHOICE: ${{ inputs.release }} - RELEASE_TAG_INPUT: ${{ inputs.release_tag }} - VERIFY_PUBLIC_URLS: ${{ inputs.verify_public_urls }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - function Test-WorkflowBool([string]$Value) { - if ([string]::IsNullOrWhiteSpace($Value)) { return $false } - return [System.Convert]::ToBoolean($Value) - } - - $dryRun = Test-WorkflowBool $env:DRY_RUN - $verifyPublicUrls = Test-WorkflowBool $env:VERIFY_PUBLIC_URLS - if (-not $dryRun -and -not $verifyPublicUrls) { - throw "verify_public_urls must be true when dry_run is false (WinGet needs anonymous download URLs)." - } - - $repo = '${{ github.repository }}' - if ($env:RELEASE_CHOICE -eq 'latest') { - $releases = @(gh release list --repo $repo --exclude-drafts --exclude-pre-releases --limit 1 --json tagName | ConvertFrom-Json) - if ($releases.Count -lt 1 -or [string]::IsNullOrWhiteSpace([string]$releases[0].tagName)) { - throw "No non-draft, non-prerelease GitHub Release found in $repo." - } - $tag = [string]$releases[0].tagName - Write-Host "Resolved latest release tag -> $tag" - } - else { - $tag = [string]($env:RELEASE_TAG_INPUT).Trim() - if ([string]::IsNullOrWhiteSpace($tag)) { - throw "release=custom requires release_tag (for example v2.0.0)." - } - } - - if ($tag -notmatch '^v?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$') { - throw "release tag must be SemVer, optionally prefixed with v: $tag" - } - gh release view $tag --repo $repo --json tagName,isDraft,isPrerelease,assets - if ($LASTEXITCODE -ne 0) { throw "GitHub Release not found: $tag" } - - $version = $tag -replace '^[vV]', '' - "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "RELEASE_TAG=$tag" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - - name: Download released installers - env: - GH_TOKEN: ${{ github.token }} - run: | - New-Item -ItemType Directory -Force -Path release-assets | Out-Null - gh release download $env:RELEASE_TAG ` - --repo "${{ github.repository }}" ` - --pattern 'nvm-*-setup.exe' ` - --dir release-assets ` - --clobber - if ($LASTEXITCODE -ne 0) { throw "Failed to download release installers." } - Get-ChildItem -LiteralPath release-assets -File | ForEach-Object { - Write-Host "release asset -> $($_.Name) ($($_.Length) bytes)" - } - - - name: Generate WinGet manifest - env: - REPOSITORY: ${{ github.repository }} - run: | - & .\build\steps\New-WingetManifest.ps1 ` - -ReleaseTag $env:RELEASE_TAG ` - -Repository $env:REPOSITORY ` - -InstallerDirectory (Join-Path $PWD 'release-assets') ` - -OutputDirectory (Join-Path $PWD 'winget-output') - - - name: Verify public release URLs and hashes - if: inputs.verify_public_urls == true - run: | - $tag = $env:RELEASE_TAG.Trim() - $version = $tag -replace '^[vV]', '' - $failed = $false - - foreach ($arch in @('x64', 'arm64')) { - $asset = "nvm-$version-$arch-setup.exe" - $source = Join-Path $PWD "release-assets\$asset" - if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { - throw "Expected downloaded asset missing: $source" - } - - $url = "https://github.com/${{ github.repository }}/releases/download/$tag/$asset" - $download = Join-Path $env:RUNNER_TEMP $asset - try { - Invoke-WebRequest -Uri $url -OutFile $download - } - catch { - $failed = $true - Write-Warning "Public URL check failed for ${asset}: $($_.Exception.Message)" - continue - } - - $expected = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash - $actual = (Get-FileHash -LiteralPath $download -Algorithm SHA256).Hash - if ($actual -ne $expected) { - $failed = $true - Write-Warning "Public URL hash mismatch for $asset. expected=$expected actual=$actual" - continue - } - Write-Host "Public URL + SHA256 verified -> $asset" - } - - if ($failed) { - throw "Public URL verification failed. Release assets must be anonymously downloadable before WinGet publish." - } - - - name: Validate manifest with WinGet - id: validate - run: | - $version = ($env:RELEASE_TAG.Trim() -replace '^[vV]', '') - $manifestDir = Join-Path $PWD "winget-output\manifests\a\AuthorSoftware\NVMWindows\$version" - $manifest = Join-Path $manifestDir "$env:WINGET_PACKAGE_IDENTIFIER.yaml" - if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) { - throw "Version manifest missing: $manifest" - } - winget --version - winget validate --manifest $manifestDir --disable-interactivity - if ($LASTEXITCODE -ne 0) { throw "winget validate failed." } - "manifest=$manifest" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "manifest_dir=$manifestDir" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - - name: Install smoke test (optional) - id: install_test - if: inputs.install_test == true - timeout-minutes: 5 - run: | - function Write-InstallTestSkipped([string]$Reason) { - Write-Warning "Install smoke test skipped: $Reason" - "ran=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - } - - # Run the x64 Inno installer from release-assets (already downloaded above). - # winget install --manifest re-downloads from GitHub and can hang on GHA - # (elevation prompts, winget dependency resolution). - $version = $env:RELEASE_TAG.Trim() -replace '^[vV]', '' - $installer = Join-Path $PWD "release-assets\nvm-$version-x64-setup.exe" - if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) { - Write-InstallTestSkipped "release asset missing: $installer" - exit 0 - } - - Write-Host "Running local installer smoke test -> $installer" - $proc = Start-Process -FilePath $installer -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART' -PassThru -NoNewWindow - if (-not $proc.WaitForExit(240000)) { - try { $proc.Kill($true) } catch { $proc.Kill() } - Write-InstallTestSkipped "installer did not finish within 4 minutes." - exit 0 - } - if ($proc.ExitCode -ne 0) { - Write-InstallTestSkipped "installer exited with code $($proc.ExitCode)." - exit 0 - } - - $nvm = Join-Path $env:LOCALAPPDATA 'Author Software\nvm\nvm.exe' - if (-not (Test-Path -LiteralPath $nvm -PathType Leaf)) { - Write-InstallTestSkipped "nvm.exe missing after install." - exit 0 - } - - & $nvm --version 2>&1 | Write-Host - if ($LASTEXITCODE -ne 0) { - Write-InstallTestSkipped "nvm --version failed after install." - exit 0 - } - - Write-Host "Install smoke test passed." - "ran=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - - name: Uninstall test installation - if: always() && inputs.install_test == true && steps.install_test.outputs.ran == 'true' - run: | - $uninstaller = Join-Path $env:LOCALAPPDATA 'Author Software\nvm\unins000.exe' - if (Test-Path -LiteralPath $uninstaller -PathType Leaf) { - $proc = Start-Process -FilePath $uninstaller -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART' -PassThru -NoNewWindow - if (-not $proc.WaitForExit(120000)) { - try { $proc.Kill($true) } catch { $proc.Kill() } - Write-Warning "Test uninstaller did not finish within 2 minutes (ignored)." - } - elseif ($proc.ExitCode -ne 0) { - Write-Warning "Test uninstaller exited with code $($proc.ExitCode) (ignored)." - } - } - else { - Write-Host "No uninstaller found; nothing to clean up." - } - - - name: Upload generated manifest - if: always() - uses: actions/upload-artifact@v7 - with: - name: winget-manifest-${{ steps.meta.outputs.tag || github.run_id }} - path: winget-output/manifests/ - if-no-files-found: warn - retention-days: 30 - - - name: Submit to microsoft/winget-pkgs - id: submit - if: inputs.dry_run == false - env: - GH_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - MANIFEST_DIR: ${{ steps.validate.outputs.manifest_dir }} - FORK_OWNER: nvm-windows - run: | - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $false - - if ([string]::IsNullOrWhiteSpace($env:WINGET_CREATE_GITHUB_TOKEN)) { - throw "dry_run is false but secret WINGET_CREATE_GITHUB_TOKEN is empty. Set a classic PAT with public_repo and workflow for a user who can push to nvm-windows/winget-pkgs." - } - - $login = (gh api user --jq .login).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($login)) { - throw "WINGET_CREATE_GITHUB_TOKEN could not read the GitHub user. Use a classic PAT with public_repo and workflow." - } - Write-Host "WinGet token user -> $login" - - $packageId = $env:WINGET_PACKAGE_IDENTIFIER - if ([string]::IsNullOrWhiteSpace($packageId)) { throw "WINGET_PACKAGE_IDENTIFIER is empty." } - $version = ($env:RELEASE_TAG.Trim() -replace '^[vV]', '') - if ([string]::IsNullOrWhiteSpace($version)) { throw "RELEASE_TAG is empty." } - - # AuthorSoftware.NVMWindows is a new package. This pull request adds that id. - # It does not update CoreyButler.NVMforWindows. wingetcreate submit always - # writes to the token user's fork, so the pull request is opened from the org fork. - $fork = "$($env:FORK_OWNER)/winget-pkgs" - $upstream = 'microsoft/winget-pkgs' - Write-Host "Package -> $packageId version $version" - Write-Host "Fork -> $fork" - - $forkName = gh api "repos/$fork" --jq .full_name 2>$null - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($forkName)) { - Write-Host "Creating fork $fork" - gh repo fork $upstream --org $env:FORK_OWNER --clone=false - if ($LASTEXITCODE -ne 0) { throw "Could not fork $upstream into $($env:FORK_OWNER)." } - } - - $archived = (gh api "repos/$fork" --jq .archived).Trim() - if ($LASTEXITCODE -ne 0) { throw "Could not read $fork." } - if ($archived -eq 'true') { - Write-Host "Unarchiving $fork so it can accept the package branch." - gh api --method PATCH "repos/$fork" -F archived=false - if ($LASTEXITCODE -ne 0) { throw "Could not unarchive $fork." } - } - - $ready = $false - for ($i = 0; $i -lt 30; $i++) { - gh api "repos/$fork/git/ref/heads/master" 1>$null 2>$null - if ($LASTEXITCODE -eq 0) { $ready = $true; break } - Write-Host "Waiting for $fork master..." - Start-Sleep -Seconds 10 - } - if (-not $ready) { throw "Fork $fork has no master branch yet." } - - gh repo sync $fork --branch master - if ($LASTEXITCODE -ne 0) { - throw "Could not sync $fork with $upstream master. winget-pkgs updates .github/workflows, so WINGET_CREATE_GITHUB_TOKEN must be a classic PAT with public_repo and workflow. Add the workflow scope on the existing token, then re-run." - } - - $baseSha = (gh api "repos/$fork/git/ref/heads/master" --jq .object.sha).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($baseSha)) { - throw "Could not read $fork master SHA." - } - $baseTree = (gh api "repos/$fork/git/commits/$baseSha" --jq .tree.sha).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($baseTree)) { - throw "Could not read the tree for $baseSha." - } - - $outputRoot = (Resolve-Path (Join-Path $env:GITHUB_WORKSPACE 'winget-output')).Path - $manifestFiles = @(Get-ChildItem -LiteralPath $env:MANIFEST_DIR -File) - if ($manifestFiles.Count -lt 1) { throw "No manifest files in $($env:MANIFEST_DIR)." } - - $tree = New-Object System.Collections.Generic.List[object] - foreach ($file in $manifestFiles) { - $relative = [System.IO.Path]::GetRelativePath($outputRoot, $file.FullName).Replace('\', '/') - if ($relative -notlike 'manifests/*') { throw "Manifest path is outside manifests/: $relative" } - $tree.Add([ordered]@{ - path = $relative - mode = '100644' - type = 'blob' - content = [System.IO.File]::ReadAllText($file.FullName) - }) - } - - $utf8 = [System.Text.UTF8Encoding]::new($false) - $treeJsonPath = Join-Path $env:RUNNER_TEMP 'winget-tree.json' - $treeJson = @{ base_tree = $baseTree; tree = $tree } | ConvertTo-Json -Depth 6 -Compress - [System.IO.File]::WriteAllText($treeJsonPath, $treeJson, $utf8) - $treeSha = (gh api --method POST "repos/$fork/git/trees" --input $treeJsonPath --jq .sha).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($treeSha)) { - throw "Could not create the manifest tree on $fork." - } - - $segments = $packageId.Split('.') - $packagePath = "manifests/$($segments[0].Substring(0,1).ToLowerInvariant())/$($segments -join '/')" - gh api "repos/$upstream/contents/$packagePath" 1>$null 2>$null - if ($LASTEXITCODE -eq 0) { - $title = "Add version: $packageId version $version" - } - else { - $title = "New package: $packageId version $version" - } - Write-Host "Pull request title -> $title" - - $commitJsonPath = Join-Path $env:RUNNER_TEMP 'winget-commit.json' - $commitJson = @{ message = $title; tree = $treeSha; parents = @($baseSha) } | ConvertTo-Json -Depth 5 -Compress - if ($commitJson -notmatch '"parents"\s*:\s*\[') { - throw "Commit parents were not serialized as a JSON array." - } - [System.IO.File]::WriteAllText($commitJsonPath, $commitJson, $utf8) - $commitSha = (gh api --method POST "repos/$fork/git/commits" --input $commitJsonPath --jq .sha).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($commitSha)) { - throw "Could not create the manifest commit on $fork." - } - - $branch = "$packageId-$version-$([guid]::NewGuid().ToString('n').Substring(0, 8))" - gh api --method POST "repos/$fork/git/refs" -f ref="refs/heads/$branch" -f sha="$commitSha" - if ($LASTEXITCODE -ne 0) { throw "Could not create branch $branch on $fork." } - - $bodyPath = Join-Path $env:RUNNER_TEMP 'winget-pr-body.md' - $tag = $env:RELEASE_TAG.Trim() - $body = @" -$title - -Node.js version manager for Windows. -https://github.com/$($env:GITHUB_REPOSITORY)/releases/tag/$tag -"@ - [System.IO.File]::WriteAllText($bodyPath, $body.Trim() + "`n", $utf8) - - $prOutput = gh pr create --repo $upstream --base master --head "$($env:FORK_OWNER):$branch" --title $title --body-file $bodyPath - if ($LASTEXITCODE -ne 0) { throw "Could not open the winget-pkgs pull request." } - $prUrl = @($prOutput -split "`r?`n" | Where-Object { $_ -match '^https://github\.com/.+/pull/\d+\s*$' } | ForEach-Object { $_.Trim() }) | Select-Object -Last 1 - if ([string]::IsNullOrWhiteSpace($prUrl)) { - throw "winget-pkgs pull request was created but no URL was returned. Output: $prOutput" - } - Write-Host "Pull request -> $prUrl" - Write-Host "::notice title=WinGet pull request::$prUrl" - "pull_request_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - [System.IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, "### WinGet pull request`n`n$prUrl`n", $utf8) diff --git a/.gitignore b/.gitignore index 6b71f28e..a86bc4cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,34 @@ -.* -!.gitignore -!.gitmodules -!.gitattributes -!.github -!.github/** -!.zigversion -!build/.zigversion -bin -*.ps1 -!build.ps1 -!build/ -!build/**/ -!build/**/*.ps1 -!installer/ -!installer/**/ -!installer/**/*.ps1 -!src/dev.ps1 -*.bat -!installer/**/*.bat -*.exe -*.dll -*.pdb -*.log +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +dist +src/v* +bin/*.exe +!bin/buildtools/* +bin/*.zip +bin/nvm/* + +!.gitignore + diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 25e19ba8..00000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "common"] - path = common - url = git@github.com:nvm-windows/common.git -[submodule "cli"] - path = cli - url = git@github.com:nvm-windows/cli.git -[submodule "sync"] - path = sync - url = git@github.com:nvm-windows/sync.git -[submodule "shim"] - path = shim - url = git@github.com:nvm-windows/shim.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7af93aba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## Before you submit an issue.... + +There have been a lot of duplicate issues filed. Please do a simple search for your issue before posting something new. + +If you just have a question and no real problem, please use the [Gitter channel](https://gitter.im/coreybutler/nvm-windows) instead of clogging up the bug tracker with questions. diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md deleted file mode 100644 index 83265055..00000000 --- a/INSTRUCTIONS.md +++ /dev/null @@ -1,60 +0,0 @@ -# Build Script Instructions - -Run from the repository root: - -```powershell -.\build\main.ps1 -``` - -`.\build.ps1` forwards to the same script. - -Default behavior: - -- Builds for the current machine architecture when not specified. -- Builds components in sequence: cli, shims, sync.exe, Inno Setup. -- Unsigned. No SBOM / SLSA / ADMX / worker DLL compile. -- Sync worker DLLs are **not** built here (certified CDN). - -Builds write executables to `.\bin` and the installer to `.\.dist`. - -## Parameters - -### -Architecture - -Allowed: `amd64`, `arm64`. Omit to auto-detect. - -```powershell -.\build\main.ps1 -Architecture amd64 -``` - -### -Component - -`All` (default), `Cli`, `Shims`, or `Sync`. Installer runs only for `All`. - -### -SkipInstaller - -Build binaries only (skip Inno Setup). - -### -BinRoot - -Override output directory (default `.\bin`). - -## Exit Codes - -- 0: success -- non-zero: first failed step (script stops) - -## Output - -Successful full build: - -- `bin\nvm.exe` -- `bin\.shim\node.exe` -- `bin\utils\proxy.exe`, `reshim.exe`, `sync.exe` -- `.dist\nvm--x64-setup.exe` or `.dist\nvm--arm64-setup.exe` (`-Architecture amd64` writes `x64`) - -## Troubleshooting - -- Installer fails: confirm `cli/src/manifest.json` has `version`, `appUserModelId`, `appId`, and Inno Setup 6.7.1+ (`ISCC.exe`) is installed. -- Sync build fails on `enhanced/`: community script rewrites `sync/src/go.mod` for the stub `common/mirrorauth` during the build and restores it after. Do not commit a dirty `sync/src/go.mod`. -- CI: see [build/README.md](build/README.md). diff --git a/LICENSE b/LICENSE index c106f114..e66cb5e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2026 Author Software Inc. +Copyright (c) 2014 Corey Butler and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b37f5fb0..d1bb315c 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,154 @@ -nvm-windows | Trendshift - -# NVM for Windows - -The Microsoft/Google recommended Node.js version manager for millions of Windows developers. - -> [!IMPORTANT] -> **You may already have NVM for Windows.** [Microsoft Zenith](https://blogs.windows.com/windowsdeveloper/2026/09/04/announcing-project-zenith-the-ready-to-code-windows-experience/) preconfigures NVM, starting with AMD Ryzen AI Halo devices. More devices from OEM and silicon partners will be available in the coming months. - -
-NVM for Windows is not the same thing as nvm! (expand for details) - -_The original [nvm](https://github.com/nvm-sh/nvm) is a completely separate project for Mac/Linux only._ This project uses an entirely different philosophy and is not just a clone of nvm. -
- - -[![Install Stable Version](https://img.shields.io/badge/-Install%20Stable%20Version-%2322A6F2)](https://github.com/nvm-windows/nvm/releases/tag/v2.0.1) ![Downloads](https://img.shields.io/github/downloads/nvm-windows/nvm/total?label=Downloads&style=social) [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fhashtags%3Dnodejs%26original_referer%3Dhttp%253A%252F%252F127.0.0.1%253A91%252F%26text%3DNVM%2520for%2520Windows%2520v2%2520is%2520available%21%26tw_p%3Dtweetbutton%26url%3Dhttps%253A%252F%252Fnvm-windows.com)](https://twitter.com/intent/tweet?hashtags=nodejs&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=NVM%20for%20Windows%20v2%20is%20available.&tw_p=tweetbutton&url=https%3A%2F%2Fnvm-windows.com) - - - - - - - - - -
-

πŸš€ Version 2 is available

-

NVM for Windows has been fully rewritten for modern workflows.

-

Explore what’s new β†’

-
-

🏒 Introducing Certified Builds

-

Deploy NVM for Windows in controlled environments with commercial Certified Builds and optional add-ons:

-
    -
  • IT-managed deployment with MSI and Intune
  • -
  • Centralized policy enforcement
  • -
  • Auditable operations with advanced logging
  • -
  • Supply-chain confidence with verifiable trust artifacts
  • -
-

Explore Certified Builds β†’

-
- -## Resources - -- [Official Website](https://nvm-windows.com) -- [Documentation](https://docs.nvm-windows.com) -- [Announcements](https://github.com/orgs/nvm-windows/discussions/categories/announcements) -- [Why We Rewrote NVM for Windows](https://medium.com/@goldglovecb/why-we-rewrote-nvm-for-windows-3b6fa5be3e7f?sharedUserId=goldglovecb) - -## Features - -> [!IMPORTANT] -> **Community Edition installers are now code-signed as of v2.0.0-hotfix.2.** - -|Feature|Description| -|:-|:-| -|Compatibility


|• No mandatory administrator privileges.
• Shim mode - no symlinks, fast (written in Zig).
• Link mode - Zero-latency, junctions with symlink fallback.| -|Automation


|• Per-directory version switching (pinning).
• Auto-install missing versions.
• Auto-install default global modules.| -|Speed



|• Parallel (multiple) simultaneous installations.
• Smaller downloads (7z).
• Native extraction.
• Caching.| -|Native Integrations



|• Windows Apps
• Logging (Windows Event Viewer)
• Windows Registry
• Desktop Notification Center| -|Customization


|• User-defined aliases.
• User-defined default global modules.
• Configure local (air-gapped) downloads.| - -### Certified Builds - -Commercial **Certified Builds** are now available for controlled environments. [Learn more](https://nvm-windows.com/certified). - -Not sure which edition is right for you? See [Choosing a Build](https://docs.nvm-windows.com/guide/builds/). - -|Feature|Description| -|:-|:-| -|IT-Managed Installation

|• Windows-protected installation directory
• Administrator-controlled deployment and device management| -|Installers

|• MSI/MST
• Microsoft Intune| -|Advanced Logging ⭐



|• Fully auditable
• Structured
• Dedicated Event Source
• Native SIEM integration| -|Policy Enforcement ⭐




|• Active Directory/Entra integration
• Restrict Node.js versions/ranges (e.g. no EOL versions, LTS only, etc.)
• Control nvm-windows, Node/npm/npx settings
• Advanced proxy (IWA, WPAD/PAC) support
• Private Node.js download mirror| -|Trust Artifacts ⭐


|• SBOM
• SLSA Provenance
• VEX Reports| - -⭐ = Add-on package - -## :pray: Thanks - -Thanks to everyone who has submitted issues on and off GitHub, made suggestions, and generally helped make this a better project. Special thanks and the full contributor list is available **[here](THANKS.md)**. +[![Tweet This!][1.1] Tweet This!][1] +[1.1]: http://i.imgur.com/wWzX9uB.png (Tweet about NVM for Windows) +[1]: https://twitter.com/intent/tweet?hashtags=nodejs&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=Check%20out%20NVM%20for%20Windows!&tw_p=tweetbutton&url=http%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows&via=goldglovecb + +[Become a Patron](https://patreon.com/coreybutler) + +## A note on Antivirus + +Several users have noticed this app being flagged as a false positive by different antivirus apps. It turns out these antivirus vendors are flagging the signature of the entire Go programming language.... basically everything written in Go is marked as a virus. See [golang/go#16292](https://github.com/golang/go/issues/16292). Details about how NVM4W is dealing with this are available in [issue #182](https://github.com/coreybutler/nvm-windows/issues/182). + +**Thanks to several users who have submitted false positive reports, this issue is gradually going away. The latest definitions for Windows Defender, Avast!, and MalwareBytes have all whitelisted the app.** + +## NOTICE: SEEKING CORE CONTRIBUTORS & MAINTAINERS + +_Are you multilingual?_ + +I am particularly interested in finding people who can speak something other than English. Several problems have come up with non-latin character sets (Chinese, Japanese, Arabic, etc). I am also interested in producing language packs/translations for the installers. + +_Are you outside of the US/UK/Canada?_ + +Custom mirroring capabilities are available in the master branch, but I would like to work with folks in different geographic regions to assure node is accessible everywhere. + +_Other (Anywhere)_ + +The core concepts of this version manager are pretty simple, so the core code base is pretty focused/simple. I've done some work to make this project available on all operating systems. The only reason it's been so slow to release is anticipation of an explosion of new installers (chocolatey, homebrew, rpm, .deb, .msi, etc). I've partnered up with BitRock to simplify creation of some of these, but the BitRock installers don't support all of these. + +Of course, I would also love to have additional maintainers. If you're new to Go, that's OK - I was too, and that's what code reviews are for. + +# Node Version Manager (nvm) for Windows + +[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/coreybutler/nvm-windows?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) (I post development updates here) + +Manage multiple installations of node.js on a Windows computer. + +**tl;dr** [nvm](https://github.com/creationix/nvm), but for Windows, with an installer. [Download Now](https://github.com/coreybutler/nvm-windows/releases)! This has always been a node version manager, not an io.js manager, so there is no back-support for io.js. However, node 4+ is supported. + +![NVM for Windows](http://coreybutler.github.io/nvm-windows/images/installlatest.jpg) + +There are situations where the ability to switch between different versions of Node.js can be very +useful. For example, if you want to test a module you're developing with the latest +bleeding edge version without uninstalling the stable version of node, this utility can help. + +![Switch between stable and unstable versions.](http://coreybutler.github.io/nvm-windows/images/use.jpg) + +### Installation & Upgrades + +It comes with an installer (and uninstaller), because getting it should be easy. Please note, you need to uninstall any existing versions of node.js before installing NVM for Windows. + +You should also delete the existing npm install location (e.g. "C:\Users\\AppData\Roaming\npm") so that the nvm install location will be correctly used instead. After install, reinstalling global utilities (e.g. gulp) will have to be done for each installed version of node: + +`nvm use 4.4.0` +`npm install gulp-cli -g` +`nvm use 0.10.33` +`npm install gulp-cli -g` + +[Download the latest installer from the releases](https://github.com/coreybutler/nvm/releases). + +![NVM for Windows Installer](http://coreybutler.github.io/nvm-windows/images/installer.jpg) + +**To upgrade**, run the new installer. It will safely overwrite the files it needs to update without touching your node.js installations. +Make sure you use the same installation and symlink folder. If you originally installed to the default locations, you just need to click +"next" on each window until it finishes. + +### Usage + +NVM for Windows is a command line tool. Simply type `nvm` in the console for help. The basic commands are: + +- `nvm arch [32|64]`: Show if node is running in 32 or 64 bit mode. Specify 32 or 64 to override the default architecture. +- `nvm install [arch]`: The version can be a node.js version or "latest" for the latest stable version. Optionally specify whether to install the 32 or 64 bit version (defaults to system arch). Set `[arch]` to "all" to install 32 AND 64 bit versions. +- `nvm list [available]`: List the node.js installations. Type `available` at the end to show a list of versions available for download. +- `nvm on`: Enable node.js version management. +- `nvm off`: Disable node.js version management (does not uninstall anything). +- `nvm proxy [url]`: Set a proxy to use for downloads. Leave `[url]` blank to see the current proxy. Set `[url]` to "none" to remove the proxy. +- `nvm uninstall `: Uninstall a specific version. +- `nvm use [arch]`: Switch to use the specified version. Optionally specify 32/64bit architecture. `nvm use ` will continue using the selected version, but switch to 32/64 bit mode based on the value supplied to ``. +- `nvm root `: Set the directory where nvm should store different versions of node.js. If `` is not set, the current root will be displayed. +- `nvm version`: Displays the current running version of NVM for Windows. + +### Gotcha! + +Please note that any global npm modules you may have installed are **not** shared between the various versions of node.js you have installed. +Additionally, some npm modules may not be supported in the version of node you're using, so be aware of your environment as you work. + +### Antivirus + +Users have reported some problems using antivirus, specifically McAffee. It appears the antivirus software is manipulating access to the VBScript engine. See [issue #133](https://github.com/coreybutler/nvm-windows/issues/133) for details and resolution. + +--- + +## Why another version manager? + +There are several version managers for node.js. Tools like [nvm](https://github.com/creationix/nvm) and [n](https://github.com/tj/n) +only run on Mac OSX and Linux. Windows users are left in the cold? No. [nvmw](https://github.com/hakobera/nvmw) and [nodist](https://github.com/marcelklehr/nodist) +are both designed for Windows. So, why another version manager for Windows? + +The architecture of most node version managers for Windows rely on `.bat` files, which do some clever tricks to set or mimic environment variables. +Some of them use node itself (once it's downloaded), which is admirable, but prone to problems. Right around node 0.10.30, the installation +structure changed a little, causing some of these to just stop working with anything new. + +Additionally, some users struggle to install these modules since it requires a little more knowledge of node's installation structure. I believe if it +were easier for people to switch between versions, people might take the time to test their code on back and future versions... which is +just good practice. + +## What's the big difference? + +First and foremost, this version of nvm has no dependency on node. It's written in [Go](https://golang.org/), which is a much more structured +approach than hacking around a limited `.bat` file. It does not rely on having an existing node installation. Plus, should the need arise, Go +offers potential for creating a Mac/Linux version on the same code base with a substanially easier migration path than converting a bunch of +batch to shell logic. `bat > sh, it crazy, right?` + +The control mechanism is also quite different. There are two general ways to support multiple node installations with hot switching capabilities. +The first is to modify the system `PATH` any time you switch versions, or bypass it by using a `.bat` file to mimic the node executable and redirect +accordingly. This always seemed a little hackish to me, and there are some quirks as a result of this implementation. + +The second option is to use a symlink. This concept requires putting the symlink in the system `PATH`, then updating its target to +the node installation directory you want to use. This is a straightforward approach, and seems to be what people recommend.... until they +realize just how much of a pain symlinks are on Windows. This is why it hasn't happened before. + +In order to create/modify a symlink, you must be running as an admin, and you must get around Windows UAC (that annoying prompt). Luckily, this is +a challenge I already solved with some helper scripts in [node-windows](https://github.com/coreybutler/node-windows). As a result, NVM for Windows +maintains a single symlink that is put in the system `PATH` during installation only. Switching to different versions of node is a matter of +switching the symlink target. As a result, this utility does **not** require you to run `nvm use x.x.x` every time you open a console window. +When you _do_ run `nvm use x.x.x`, the active version of node is automatically updated across all open console windows. It also persists +between system reboots, so you only need to use nvm when you want to make a change. + +NVM for Windows comes with an installer, courtesy of a byproduct of my work on [Fenix Web Server](http://fenixwebserver.com). + +Overall, this project brings together some ideas, a few battle-hardened pieces of other modules, and support for newer versions of node. + +I also wrote a simple [data feed](https://github.com/coreybutler/nodedistro) containing a list of node.js versions and their associated npm version. +This is how NVM for Windows recognizes the "latest" stable version. It's free for anyone to use. + +## Motivation + +I needed it, plain and simple. Additionally, it's apparent that [support for multiple versions](https://github.com/nodejs/node-v0.x-archive/issues/8075) is not +coming to node core, or even something they care about. It was also an excuse to play with Go. + +## Why Go? Why not Node? + +I chose Go because it is cross-platform, felt like less overhead than Java, has been around longer than most people think, and I wanted to experiment with it. I've been asked why I didn't write it with Node. Trying to write a tool with the tool you're trying to install doesn't make sense to me. As a result, my project requirements for this were simple... something that's not Node. Node will continue to evolve and change. If you need a reminder of that, io.js. Or consider all the breaking changes between 4.x.x and 6.x.x. These are inevitable in the world of software. + +## License + +MIT. + +## Thanks + +Thanks to everyone who has submitted issues on and off Github, made suggestions, and generally helped make this a better project. Special thanks to [@vkbansal](https://github.com/vkbansal), who has actively provided feedback throughout the releases. Thanks also go to [@rainabba](https://github.com/rainabba) and [@sullivanpt](https://github.com/sullivanpt) for getting Node v4 support integrated. + +## Alternatives + +- [nodist](https://github.com/marcelklehr/nodist) - Windows Only +- [nvm](https://github.com/creationix/nvm) - Mac/Linux Only +- [n](https://github.com/visionmedia/n) - Mac/Linux Only diff --git a/THANKS.md b/THANKS.md deleted file mode 100644 index 13fdbe0e..00000000 --- a/THANKS.md +++ /dev/null @@ -1,113 +0,0 @@ -# Thanks - -Thanks to everyone who has submitted issues on and off GitHub, made suggestions, and generally helped make this a better project. - -## Special thanks - -- [@vkbansal](https://github.com/vkbansal), who provided significant early feedback throughout the early releases. -- [@rainabba](https://github.com/rainabba) and [@sullivanpt](https://github.com/sullivanpt) for getting Node v4 support integrated. -- [@s-h-a-d-o-w](https://github.com/s-h-a-d-o-w) who resolved the longstanding space escaping issue in path names ([#355](https://github.com/nvm-windows/nvm/pull/355)). -- [ajyong](https://github.com/ajyong) who sponsored the code signing certificate in late 2021. - -## Contributors - - -_Commit authors on `master` (v1) and `main` (v2), sorted by commit count. Bots omitted. Updated on each community release._ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
coreybutler
coreybutler

540
gbdrummer
gbdrummer

10
wsobolewski
wsobolewski

7
Excited-ccccly
Excited-ccccly

4
rahulroy9202
rahulroy9202

4
lijie371
lijie371

3
mscott3
mscott3

3
narve
narve

3
noelhibbard
noelhibbard

3
CyBot
CyBot

2
fredericrous
fredericrous

2
jchitel-ds
jchitel-ds

2
nineninesevenfour
nineninesevenfour

2
rainabba
rainabba

2
s-h-a-d-o-w
s-h-a-d-o-w

2
sitiom
sitiom

2
taaem
taaem

2
TomasHubelbauer
TomasHubelbauer

2
tomisawk
tomisawk

2
veleek
veleek

2
0liver
0liver

1
acdvorak
acdvorak

1
akost
akost

1
alanblins
alanblins

1
alkuzad
alkuzad

1
asakaxgit
asakaxgit

1
awdr74100
awdr74100

1
bchr02
bchr02

1
benediktwerner
benediktwerner

1
bennycode
bennycode

1
clarkttfu
clarkttfu

1
coffee-converter
coffee-converter

1
crhistianramirez
crhistianramirez

1
davidribyrne
davidribyrne

1
detailyang
detailyang

1
EEJesse
EEJesse

1
fredericosilva
fredericosilva

1
gdziadkiewicz
gdziadkiewicz

1
GitTom
GitTom

1
gravewhisper
gravewhisper

1
himself65
himself65

1
hn3000
hn3000

1
inoc603
inoc603

1
jeffrson
jeffrson

1
Josh68
Josh68

1
jpm63
jpm63

1
Juneezee
Juneezee

1
kylewuolle
kylewuolle

1
leongjiameng
leongjiameng

1
mikemaccana
mikemaccana

1
mikesigs
mikesigs

1
miparnisari
miparnisari

1
MizardX
MizardX

1
mon-jai
mon-jai

1
Nasfame
Nasfame

1
psnitil
psnitil

1
qiulaidongfeng
qiulaidongfeng

1
rbuckton
rbuckton

1
rcknight
rcknight

1
ReadmeCritic
ReadmeCritic

1
riazXrazor
riazXrazor

1
romualdr
romualdr

1
ryanclarke
ryanclarke

1
stefan-ptrvch
stefan-ptrvch

1
sullivanpt
sullivanpt

1
tathamoddie
tathamoddie

1
tats-u
tats-u

1
Tester798
Tester798

1
toureholder
toureholder

1
VISHNU-LMNTRIX
VISHNU-LMNTRIX

1
vladonemo
vladonemo

1
WORMSS
WORMSS

1
xmedeko
xmedeko

1
zyonet
zyonet

1
- diff --git a/bin/elevate.cmd b/bin/elevate.cmd new file mode 100644 index 00000000..ba1bf4b9 --- /dev/null +++ b/bin/elevate.cmd @@ -0,0 +1,5 @@ +@setlocal +@echo off +set CMD=%* +set APP=%1 +start wscript //nologo "%~dpn0.vbs" %* diff --git a/bin/elevate.vbs b/bin/elevate.vbs new file mode 100644 index 00000000..c56b8f78 --- /dev/null +++ b/bin/elevate.vbs @@ -0,0 +1,13 @@ +Set Shell = CreateObject("Shell.Application") +Set WShell = WScript.CreateObject("WScript.Shell") +Set ProcEnv = WShell.Environment("PROCESS") + +cmd = ProcEnv("CMD") +app = ProcEnv("APP") +args= Right(cmd,(Len(cmd)-Len(app))) + +If (WScript.Arguments.Count >= 1) Then + Shell.ShellExecute app, args, "", "runas", 0 +Else + WScript.Quit +End If diff --git a/bin/install.cmd b/bin/install.cmd new file mode 100644 index 00000000..e5183a78 --- /dev/null +++ b/bin/install.cmd @@ -0,0 +1,15 @@ +@echo off +set /P NVM_PATH="Enter the absolute path where the zip file is extracted/copied to: " +setx /M NVM_HOME "%NVM_PATH%" +setx /M NVM_SYMLINK "C:\Program Files\nodejs" +setx /M PATH "%PATH%;%NVM_HOME%;%NVM_SYMLINK%" + +if exist "%SYSTEMDRIVE%\Program Files (x86)\" ( +set SYS_ARCH=64 +) else ( +set SYS_ARCH=32 +) +(echo root: %NVM_HOME% && echo path: %NVM_SYMLINK% && echo arch: %SYS_ARCH% && echo proxy: none) > %NVM_HOME%\settings.txt + +notepad %NVM_HOME%\settings.txt +@echo on diff --git a/bin/nodejs.ico b/bin/nodejs.ico new file mode 100644 index 00000000..a4d5b1a7 Binary files /dev/null and b/bin/nodejs.ico differ diff --git a/build.bat b/build.bat new file mode 100644 index 00000000..bd5edf4c --- /dev/null +++ b/build.bat @@ -0,0 +1,60 @@ +@echo off +SET INNOSETUP=%CD%\nvm.iss +SET ORIG=%CD% +REM SET GOPATH=%CD%\src +SET GOBIN=%CD%\bin +SET GOARCH=386 +SET version=1.1.1 + +REM Get the version number from the setup file +REM for /f "tokens=*" %%i in ('findstr /n . %INNOSETUP% ^| findstr ^4:#define') do set L=%%i +REM set version=%L:~24,-1% + +REM Get the version number from the core executable +REM for /f "tokens=*" %%i in ('findstr /n . %GOPATH%\nvm.go ^| findstr ^NvmVersion^| findstr ^21^') do set L=%%i +REM set goversion=%L:~19,-1% + +REM IF NOT %version%==%goversion% GOTO VERSIONMISMATCH + +SET DIST=%CD%\dist\%version% + +REM Build the executable +echo Building NVM for Windows +REM rm %GOBIN%\nvm.exe +REM cd %GOPATH% +echo "=========================================>" +REM echo %GOBIN% +REM goxc -arch="386" -os="windows" -n="nvm" -d="%GOBIN%" -o="%GOBIN%\nvm{{.Ext}}" -tasks-=package + +REM cd %ORIG% +REM rm %GOBIN%\src.exe +REM rm %GOPATH%\src.exe +REM rm %GOPATH%\nvm.exe + +REM Clean the dist directory +rm -rf "%DIST%" +mkdir "%DIST%" + +echo Creating distribution in %DIST% + +if exist src\nvm.exe ( + rm src\nvm.exe +) + +echo "Building nvm.exe...." + +go build src\nvm.go +mv nvm.exe %GOBIN% + +echo Building "noinstall" zip... +for /d %%a in (%GOBIN%) do (buildtools\zip -j -9 -r "%DIST%\nvm-noinstall.zip" "%CD%\LICENSE" "%%a\*" -x "%GOBIN%\nodejs.ico") + +echo "Building the primary installer..." +buildtools\iscc %INNOSETUP% /o%DIST% +buildtools\zip -j -9 -r "%DIST%\nvm-setup.zip" "%DIST%\nvm-setup.exe" + +echo "Distribution created. Now cleaning up...." +rm %GOBIN%/nvm.exe + +echo "Done." +@echo on diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index f6e8fed7..00000000 --- a/build.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -# Compatibility wrapper. Canonical entrypoint: .\build\main.ps1 -& (Join-Path $PSScriptRoot "build\main.ps1") @args -if ($null -ne $LASTEXITCODE) { - exit $LASTEXITCODE -} diff --git a/build/.zigversion b/build/.zigversion deleted file mode 100644 index 4312e0d0..00000000 --- a/build/.zigversion +++ /dev/null @@ -1 +0,0 @@ -0.15.2 diff --git a/build/README.md b/build/README.md deleted file mode 100644 index a60a64e8..00000000 --- a/build/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Community build - -Authenticode-signed Inno Setup pipeline for NVM for Windows (core / community). No COSE worker signing, no SBOM, no SLSA, no ADMX, no MSI/Intune. - -Produces `nvm.exe`, ETW event provider assets (`NVMWindows.Events.man` + `NVMWindows.Events.dll`), shims (`node` / `proxy` / `reshim`), `sync.exe`, and one Inno Setup `.exe`. Sync worker DLLs come from the certified release CDN (`assets.nvm-windows.com`) β€” this build does not compile them (CDN workers are already COSE Sign1'd by the certified pipeline). - -`NVMWindows.Events.dll` is a wevtutil message/resource DLL (`/NOENTRY`). It is **Authenticode-signed** with product `.exe`/`.msi` artifacts (same Artifact Signing account). It is **not** COSE-signed (COSE is for sync workers only). - -The `sync` Git submodule is **private**. Maintainers/CI compile it from source. Public from-source builds use `-DownloadSync` to fetch the matching release asset. - -## Local - -Requires: Go (see `cli/src/go.mod`), [qgo](https://github.com/quikdev/go), Zig (`shim/.zigversion`), [Inno Setup](https://jrsoftware.org/isdl.php) 6.7.1+, `go-winres`. - -```powershell -# Maintainer / CI (sync submodule present) -.\build\main.ps1 -.\build\main.ps1 -Architecture amd64 -.\build\main.ps1 -Architecture arm64 -SkipInstaller -.\build\main.ps1 -Component Cli -.\build\main.ps1 -Hotfix beta.1 -.\build\main.ps1 -Hotfix 2 -.\build\main.ps1 -Version 2.0.1-hotfix.2 - -# Public clone (no sync source): download prebuilt sync.exe from the GitHub Release -.\build\main.ps1 -DownloadSync -.\build\main.ps1 -DownloadSync -SyncReleaseTag v2.0.0 -.\build\main.ps1 -Component Sync -DownloadSync -``` - -`.\build.ps1` still forwards to `.\build\main.ps1`. - -| Flag | Purpose | -|------|---------| -| `-Hotfix` | Same as GHA `prerelease`: any stamp β†’ `{manifest}-{stamp}` (e.g. `beta.1` β†’ `2.0.1-beta.1`). Bare digit `1` β†’ `-hotfix.1` (WiX/Inno revision). Temp-stamps `cli/src/manifest.json` for qgo embed, sets process `NVM_CLI_VERSION`, restores both after build (git stays clean). Mutually exclusive with `-Version`. | -| `-Version` | Full special version override (e.g. `2.0.1-beta.1`). Same temp stamp/restore as `-Hotfix`. Mutually exclusive with `-Hotfix`. | -| `-DownloadSync` | Fetch `nvm---sync.exe` from GitHub Releases instead of compiling sync | -| `-SyncReleaseTag` | Override release tag (default: `v` + `cli/src/manifest.json` version) | -| `-SyncReleaseRepo` | Override `owner/repo` (default: `nvm-windows/nvm`) | - -Output: - -- Executables β†’ `bin\` -- Event provider β†’ `bin\NVMWindows.Events.man`, `bin\NVMWindows.Events.dll` (shipped next to `nvm.exe` in the installer) -- Installer β†’ `.dist\nvm---setup.exe` (`-Architecture amd64` writes `x64`) -- Staged sync release asset β†’ `.dist\nvm---sync.exe` - -Requires Windows SDK (`mc.exe` / `rc.exe`) and Visual Studio `link.exe` for the event resource DLL. - -## GHA - -Workflow: [Release Community Build](../.github/workflows/release.yml) (`workflow_dispatch`). - -| Input | Default | Purpose | -|-------|---------|---------| -| `architecture` | `both` | `amd64`, `arm64`, or both | -| `publish_release` | true | Draft β†’ upload assets β†’ publish | -| `override_existing_release` | false | Replace setup.exe **and** sync.exe on existing tag | -| `release_kind` | `General` | Release Type. `General` publishes the manifest version. `alpha`, `beta`, and `rc` prerelease that manifest version. `hotfix` patches a prior general release. The workflow assigns the next number. | -| `hotfix_base` | _(empty)_ | Hotfix Mainline. Specify the general release to apply the hotfix to, for example `2.0.1`. Leave empty to use the latest general release. Ignored unless Release Type is `hotfix`. | - -GitHub Release assets per architecture: - -- `nvm--x64-setup.exe` and `nvm--arm64-setup.exe` β€” Inno Setup installers -- `nvm--x64-sync.exe` and `nvm--arm64-sync.exe` β€” prebuilt sync for `-DownloadSync` - -Tag = `v` + effective version. `General` uses `cli/src/manifest.json` (`2.0.2`). `alpha`, `beta`, and `rc` use that manifest plus the kind and the next unused number (`2.0.2-beta.3` already published means the next beta is `2.0.2-beta.4`). `hotfix` uses the Hotfix Mainline plus the next `-hotfix.N` (`2.0.1-hotfix.2` already published means the next hotfix of `2.0.1` is `2.0.1-hotfix.3`), not the current manifest. The runner patches the manifest before the CLI and Inno build so the embedded version and `AppVersion` match. Inno `VersionInfoVersion` maps the trailing `-word.N` to the fourth numeric field (`2.0.1-hotfix.1` β†’ `2.0.1.1`). Any `x.y.z-*` release is marked GitHub **`--prerelease`**. - -### WinGet - -Workflow: [Publish to WinGet](../.github/workflows/winget.yml) (`workflow_dispatch`). - -Use it only after a public GitHub Release has both `x64` and `arm64` setup assets. It: - -1. Downloads release installers. -2. Generates `AuthorSoftware.NVMWindows` manifests and SHA256 values. -3. Verifies anonymous public release URLs produce matching hashes. -4. Runs `winget validate`. -5. By default, runs a silent install smoke test from the downloaded x64 setup asset, runs `nvm --version`, then uninstalls. -6. Uploads the generated manifests as a workflow artifact. -7. When `dry_run` is **false**, opens a pull request from `nvm-windows/winget-pkgs` that adds `AuthorSoftware.NVMWindows`. The first submission is a new package. Later runs add a version. This does not update `CoreyButler.NVMforWindows`. - -`dry_run` defaults to **true** (validate only). Live submit needs secret `WINGET_CREATE_GITHUB_TOKEN` (classic PAT with `public_repo` and `workflow` for a user who can push `nvm-windows/winget-pkgs`). The `workflow` scope is required because syncing that fork fast-forwards Microsoft's Actions workflow changes. - -Inputs: - -| Input | Default | Purpose | -|-------|---------|---------| -| `release` | `latest` | `latest` = newest stable GitHub Release (no drafts/pre-releases). `custom` = use `release_tag`. | -| `release_tag` | empty | Tag for `custom` (e.g. `v2.0.0-alpha.2`). Ignored when `release=latest`. | -| `dry_run` | true | **On:** generate manifest, `winget validate`, upload artifact β€” no PR. **Off:** open a `microsoft/winget-pkgs` pull request from `nvm-windows/winget-pkgs` for `AuthorSoftware.NVMWindows`. | -| `install_test` | true | Silent install smoke test via local `release-assets` installer (not `winget install`). 4 min process timeout + 5 min step cap. Warns/skips on failure; never fails dry-run. | -| `verify_public_urls` | true | Anonymous GitHub release download + SHA256 must match manifest hashes. Fails workflow on mismatch (dry-run or publish). | - -### Secrets - -| Name | Purpose | -|------|---------| -| `GH_APP_CLIENT_ID` | GitHub App client ID (submodule checkout) | -| `GH_APP_PRIVATE_KEY` | App private key PEM | -| `WINGET_CREATE_GITHUB_TOKEN` | Classic PAT (`public_repo` and `workflow`) that can push `nvm-windows/winget-pkgs` when `dry_run=false` | - -App install on `nvm-windows` must include **nvm**, **cli**, **common**, **shim**, **sync** (or all repos), **Contents: Read**. Inno Setup lives in-repo at `installer/` (not a submodule). Release publish uses `github.token` (`contents: write`). - -Immutable releases: published tag locks forever. Bump manifest version, or turn immutable off and set `override_existing_release`. diff --git a/build/common.ps1 b/build/common.ps1 deleted file mode 100644 index fb56a783..00000000 --- a/build/common.ps1 +++ /dev/null @@ -1,629 +0,0 @@ -# Shared paths/helpers for community nvm/build. -# Dot-source from main.ps1 or step scripts; then call Initialize-NvmBuildContext. - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$script:NvmBuildDir = $PSScriptRoot -$script:NvmRepoRoot = Split-Path -Parent $script:NvmBuildDir - -function Get-NvmRepoRoot { - return $script:NvmRepoRoot -} - -function Get-NvmBuildDir { - return $script:NvmBuildDir -} - -function Get-NvmDefaultBinRoot { - return (Join-Path $script:NvmRepoRoot "bin") -} - -function Get-NvmDistRoot { - return (Join-Path $script:NvmRepoRoot ".dist") -} - -function Format-NvmByteSize { - param([long]$Bytes) - - if ($Bytes -ge 1GB) { - return ("{0:N1} GB" -f ($Bytes / 1GB)) - } - if ($Bytes -ge 1MB) { - return ("{0:N1} MB" -f ($Bytes / 1MB)) - } - if ($Bytes -ge 1KB) { - return ("{0:N1} KB" -f ($Bytes / 1KB)) - } - return "$Bytes B" -} - -function Get-NvmCliManifestPath { - return (Join-Path $script:NvmRepoRoot "cli\src\manifest.json") -} - -function Get-NvmCliVersion { - # GHA / packaging: set NVM_CLI_VERSION (e.g. prepare.outputs.version with hotfix stamp). - $fromEnv = [Environment]::GetEnvironmentVariable("NVM_CLI_VERSION") - if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { - return $fromEnv.Trim() - } - $manifestPath = Get-NvmCliManifestPath - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "CLI manifest not found: $manifestPath" - } - $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json - $version = [string]$manifest.version - if ([string]::IsNullOrWhiteSpace($version)) { - throw "CLI manifest does not define version: $manifestPath" - } - return $version -} - -function Get-NvmLatestStableReleaseVersion { - param( - [string]$Repo = "nvm-windows/nvm" - ) - - $repo = $Repo.Trim() - if ([string]::IsNullOrWhiteSpace($repo)) { - throw "Get-NvmLatestStableReleaseVersion: Repo is empty" - } - - $raw = & gh api --paginate "repos/$repo/releases?per_page=100" - if ($LASTEXITCODE -ne 0) { - throw "Get-NvmLatestStableReleaseVersion: gh api repos/$repo/releases failed with exit code $LASTEXITCODE" - } - if ([string]::IsNullOrWhiteSpace($raw)) { - throw "Get-NvmLatestStableReleaseVersion: no GitHub releases for $repo. Fill in Hotfix base." - } - - $releases = @($raw | ConvertFrom-Json) - foreach ($rel in $releases) { - if ($rel.prerelease -or $rel.draft) { continue } - $tag = [string]$rel.tag_name - if ($tag -match '^v?(?\d+\.\d+\.\d+)$') { - return $Matches["ver"] - } - } - - throw "Get-NvmLatestStableReleaseVersion: no stable x.y.z release found for $repo. Fill in Hotfix base with a version such as 2.0.1." -} - -function Get-NvmNextPrereleaseNumberFromNames { - param( - [AllowEmptyCollection()] - [string[]]$Names = @(), - [Parameter(Mandatory = $true)] - [string]$Version, - [Parameter(Mandatory = $true)] - [string]$Kind - ) - - $version = $Version.Trim() -replace '^[vV]', '' - $kind = $Kind.Trim().ToLowerInvariant() - if ($version -notmatch '^\d+\.\d+\.\d+$') { - throw ("Get-NvmNextPrereleaseNumberFromNames: version '{0}' must be major.minor.patch." -f $version) - } - $allowed = @("alpha", "beta", "rc", "hotfix") - if ($allowed -notcontains $kind) { - throw ("Get-NvmNextPrereleaseNumberFromNames: kind '{0}' must be alpha, beta, rc, or hotfix." -f $kind) - } - - $pattern = '^[vV]?' + [regex]::Escape($version) + '-' + [regex]::Escape($kind) + '\.(?[1-9]\d*)$' - $max = [long]0 - foreach ($name in @($Names)) { - if ([string]::IsNullOrWhiteSpace($name)) { continue } - if ($name.Trim() -match $pattern) { - $n = [long]$Matches["n"] - if ($n -gt $max) { $max = $n } - } - } - return ($max + 1) -} - -function Get-NvmNextPrereleaseNumber { - param( - [Parameter(Mandatory = $true)] - [string]$Version, - [Parameter(Mandatory = $true)] - [string]$Kind, - [string]$Repo = "nvm-windows/nvm" - ) - - $repo = $Repo.Trim() - if ([string]::IsNullOrWhiteSpace($repo)) { - throw "Get-NvmNextPrereleaseNumber: Repo is empty" - } - - $names = New-Object System.Collections.Generic.List[string] - $releaseRaw = & gh api --paginate "repos/$repo/releases?per_page=100" - if ($LASTEXITCODE -ne 0) { - throw "Get-NvmNextPrereleaseNumber: gh api repos/$repo/releases failed with exit code $LASTEXITCODE" - } - if (-not [string]::IsNullOrWhiteSpace($releaseRaw)) { - foreach ($rel in @($releaseRaw | ConvertFrom-Json)) { - $tag = [string]$rel.tag_name - if (-not [string]::IsNullOrWhiteSpace($tag)) { $names.Add($tag) } - } - } - - $tagRaw = & gh api --paginate "repos/$repo/tags?per_page=100" - if ($LASTEXITCODE -ne 0) { - throw "Get-NvmNextPrereleaseNumber: gh api repos/$repo/tags failed with exit code $LASTEXITCODE" - } - if (-not [string]::IsNullOrWhiteSpace($tagRaw)) { - foreach ($tag in @($tagRaw | ConvertFrom-Json)) { - $name = [string]$tag.name - if (-not [string]::IsNullOrWhiteSpace($name)) { $names.Add($name) } - } - } - - return (Get-NvmNextPrereleaseNumberFromNames -Names @($names) -Version $Version -Kind $Kind) -} - -function Resolve-NvmReleaseVersion { - param( - [Parameter(Mandatory = $true)] - [string]$ManifestVersion, - [string]$Kind = "none", - [string]$PrereleaseNumber = "", - [string]$HotfixBase = "" - ) - - $manifest = $ManifestVersion.Trim() - if ($manifest -notmatch '^\d+\.\d+\.\d+$') { - throw ("Resolve-NvmReleaseVersion: manifest version '{0}' must be major.minor.patch." -f $manifest) - } - - $kind = if ([string]::IsNullOrWhiteSpace($Kind)) { "none" } else { $Kind.Trim().ToLowerInvariant() } - if ($kind -eq "general") { $kind = "none" } - $allowed = @("none", "alpha", "beta", "rc", "hotfix") - if ($allowed -notcontains $kind) { - throw ("Resolve-NvmReleaseVersion: release kind '{0}' must be none, alpha, beta, rc, or hotfix." -f $kind) - } - - $number = if ($null -eq $PrereleaseNumber) { "" } else { $PrereleaseNumber.Trim() } - $hotfix = if ($null -eq $HotfixBase) { "" } else { $HotfixBase.Trim() } - $hotfix = $hotfix -replace '^[vV]', '' - - if ($kind -eq "none") { - if (-not [string]::IsNullOrWhiteSpace($number)) { - throw "Resolve-NvmReleaseVersion: Prerelease number is only used for alpha, beta, rc, and hotfix. Leave it empty when Release kind is none." - } - return $manifest - } - - $series = $manifest - if ($kind -eq "hotfix") { - if ([string]::IsNullOrWhiteSpace($hotfix)) { - throw "Resolve-NvmReleaseVersion: Hotfix base is empty. Fill in a prior stable version such as 2.0.1, or leave the form empty so the latest non-prerelease release is used." - } - if ($hotfix -notmatch '^\d+\.\d+\.\d+$') { - throw ("Resolve-NvmReleaseVersion: Hotfix base '{0}' must be major.minor.patch with no prerelease suffix." -f $hotfix) - } - $series = $hotfix - } - - if ([string]::IsNullOrWhiteSpace($number)) { - $number = [string](Get-NvmNextPrereleaseNumber -Version $series -Kind $kind) - Write-Host ("Prerelease number -> {0} (next {1} for {2})" -f $number, $kind, $series) - } - elseif ($number -notmatch '^[1-9]\d*$') { - throw ("Resolve-NvmReleaseVersion: Prerelease number '{0}' must be a positive integer." -f $number) - } - - return "{0}-{1}.{2}" -f $series, $kind, $number -} - -function Resolve-NvmHotfixVersion { - param( - [Parameter(Mandatory = $true)] - [string]$BaseVersion, - # Optional prerelease stamp (workflow input name may be prerelease/hotfix). - [Alias("Prerelease")] - [string]$Hotfix = "" - ) - - $base = $BaseVersion.Trim() - if ([string]::IsNullOrWhiteSpace($base)) { - throw "Resolve-NvmHotfixVersion: BaseVersion is empty" - } - if ($base -notmatch '^\d+\.\d+\.\d+$') { - throw ("Resolve-NvmHotfixVersion: base version '{0}' must be major.minor.patch (no prerelease). Stamp via workflow input instead." -f $base) - } - - $raw = if ($null -eq $Hotfix) { "" } else { $Hotfix.Trim() } - if ([string]::IsNullOrWhiteSpace($raw)) { - return $base - } - - # Allow accidental "-beta.1" / "vbeta.1" from copy-paste. - $raw = $raw -replace '^[vV-]+', '' - if ([string]::IsNullOrWhiteSpace($raw)) { - return $base - } - - $suffix = $null - if ($raw -match '^\d+$') { - # Bare digit keeps historical hotfix.N (WiX/Inno revision mapping). - $suffix = "hotfix.$raw" - } - elseif ($raw -match '^[A-Za-z0-9][A-Za-z0-9.-]*$') { - # Any semver-ish prerelease id: beta.1, hotfix.1, rc.1, preview.3, … - $suffix = $raw - } - else { - throw @" -Resolve-NvmHotfixVersion: invalid prerelease stamp '$raw'. -Use empty (manifest version), a digit (1 -> -hotfix.1), or a stamp like beta.1 / hotfix.1 / rc.1. -"@ - } - - return "{0}-{1}" -f $base, $suffix -} - -function Set-NvmCliManifestVersion { - param( - [Parameter(Mandatory = $true)] - [string]$Version - ) - - $ver = $Version.Trim() - if ([string]::IsNullOrWhiteSpace($ver)) { - throw "Set-NvmCliManifestVersion: Version is empty" - } - - $manifestPath = Get-NvmCliManifestPath - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "CLI manifest not found: $manifestPath" - } - - $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json - $before = [string]$manifest.version - $manifest.version = $ver - - $json = $manifest | ConvertTo-Json -Depth 30 - $utf8NoBom = New-Object System.Text.UTF8Encoding $false - [System.IO.File]::WriteAllText($manifestPath, ($json.TrimEnd() + "`n"), $utf8NoBom) - - Write-Host ("CLI manifest version -> {0} (was {1}) [{2}]" -f $ver, $before, $manifestPath) -} - -function Use-NvmZigOnPath { - $existing = Get-Command zig -ErrorAction SilentlyContinue - if ($existing -and (Test-Path -LiteralPath $existing.Source -PathType Leaf)) { - return $existing.Source - } - - $wanted = "" - $zigVersionFile = Join-Path $script:NvmRepoRoot "shim\.zigversion" - if (Test-Path -LiteralPath $zigVersionFile -PathType Leaf) { - $wanted = (Get-Content -LiteralPath $zigVersionFile -TotalCount 1).Trim() - } - - $candidates = [System.Collections.Generic.List[string]]::new() - if (-not [string]::IsNullOrWhiteSpace($wanted)) { - $candidates.Add((Join-Path $env:LOCALAPPDATA "Programs\zig-$wanted\zig.exe")) - $candidates.Add((Join-Path $env:LOCALAPPDATA "zig-$wanted\zig.exe")) - } - $candidates.Add((Join-Path $env:LOCALAPPDATA "Programs\zig\zig.exe")) - - foreach ($exe in $candidates) { - if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { - continue - } - $dir = Split-Path -Parent $exe - if ($env:Path -notlike "*$dir*") { - $env:Path = "$dir;$env:Path" - } - Write-Host "zig (session PATH) -> $exe" - return $exe - } - - $hint = if ([string]::IsNullOrWhiteSpace($wanted)) { "zig" } else { "zig $wanted" } - throw @" -$hint not on PATH. This PowerShell session was opened before Zig was installed. -Install from https://ziglang.org/download/ then reopen the terminal, or: - `$env:Path = `"`$env:LOCALAPPDATA\Programs\zig-$wanted;`" + `$env:Path -"@ -} - -function Initialize-NvmBuildContext { - param( - [string]$BinRoot = "", - [switch]$SkipZig - ) - - # Release version gate and manifest stamp run before setup-zig. - if (-not $SkipZig) { - Use-NvmZigOnPath | Out-Null - } - - $resolvedBin = if ([string]::IsNullOrWhiteSpace($BinRoot)) { - Get-NvmDefaultBinRoot - } - else { - [System.IO.Path]::GetFullPath($BinRoot) - } - - New-Item -ItemType Directory -Force -Path $resolvedBin | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $resolvedBin "utils") | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $resolvedBin ".shim") | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $resolvedBin ".sync") | Out-Null - - return [pscustomobject]@{ - RepoRoot = $script:NvmRepoRoot - BuildDir = $script:NvmBuildDir - BinRoot = $resolvedBin - DistRoot = (Get-NvmDistRoot) - CliVersion = (Get-NvmCliVersion) - } -} - -function Assert-NvmFile { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [string]$Label = "file" - ) - - $full = [System.IO.Path]::GetFullPath($Path) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw ("Expected {0} missing: {1}" -f $Label, $full) - } - return $full -} - -function Get-NvmExpectedExePaths { - param( - [Parameter(Mandatory = $true)] - [string]$BinRoot, - [ValidateSet("All", "Cli", "Shims", "Sync")] - [string]$Component = "All" - ) - - $paths = New-Object System.Collections.Generic.List[string] - switch ($Component) { - "Cli" { - $paths.Add((Join-Path $BinRoot "nvm.exe")) - } - "Shims" { - $paths.Add((Join-Path $BinRoot ".shim\node.exe")) - $paths.Add((Join-Path $BinRoot "utils\proxy.exe")) - $paths.Add((Join-Path $BinRoot "utils\reshim.exe")) - } - "Sync" { - $paths.Add((Join-Path $BinRoot "utils\sync.exe")) - } - "All" { - $paths.Add((Join-Path $BinRoot "nvm.exe")) - $paths.Add((Join-Path $BinRoot ".shim\node.exe")) - $paths.Add((Join-Path $BinRoot "utils\proxy.exe")) - $paths.Add((Join-Path $BinRoot "utils\reshim.exe")) - $paths.Add((Join-Path $BinRoot "utils\sync.exe")) - } - } - return $paths.ToArray() -} - -function Get-NvmExpectedEventProviderPaths { - param( - [Parameter(Mandatory = $true)] - [string]$BinRoot - ) - - $manifestPath = Get-NvmCliManifestPath - $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json - $manName = [string]$manifest.eventLabel - if ([string]::IsNullOrWhiteSpace($manName)) { - $manName = "NVMWindows.Events.man" - } - $dllName = [System.IO.Path]::ChangeExtension($manName, ".dll") - return @( - (Join-Path $BinRoot $manName), - (Join-Path $BinRoot $dllName) - ) -} - -function Get-NvmReleaseArchToken { - param( - [Parameter(Mandatory = $true)] - [ValidateSet("amd64", "arm64")] - [string]$Architecture - ) - # Go/build target stays amd64. Published installer and sync asset names use x64. - if ($Architecture -eq "amd64") { - return "x64" - } - return $Architecture -} - -function Get-NvmInstallerSetupPath { - param( - [Parameter(Mandatory = $true)] - [string]$Version, - [Parameter(Mandatory = $true)] - [ValidateSet("amd64", "arm64")] - [string]$Architecture, - [string]$DistRoot = "" - ) - - if ([string]::IsNullOrWhiteSpace($DistRoot)) { - $DistRoot = Get-NvmDistRoot - } - # Inno OutputBaseFilename: nvm-{version}-{x64|arm64}-setup - $archToken = Get-NvmReleaseArchToken -Architecture $Architecture - return (Join-Path $DistRoot ("nvm-{0}-{1}-setup.exe" -f $Version, $archToken)) -} - -function Get-NvmSyncReleaseAssetPath { - param( - [Parameter(Mandatory = $true)] - [string]$Version, - [Parameter(Mandatory = $true)] - [ValidateSet("amd64", "arm64")] - [string]$Architecture, - [string]$DistRoot = "" - ) - - if ([string]::IsNullOrWhiteSpace($DistRoot)) { - $DistRoot = Get-NvmDistRoot - } - # Staged next to setup for GitHub Release upload / -DownloadSync fetch - $archToken = Get-NvmReleaseArchToken -Architecture $Architecture - return (Join-Path $DistRoot ("nvm-{0}-{1}-sync.exe" -f $Version, $archToken)) -} - -function Get-NvmSyncReleaseAssetName { - param( - [Parameter(Mandatory = $true)] - [string]$Version, - [Parameter(Mandatory = $true)] - [ValidateSet("amd64", "arm64")] - [string]$Architecture - ) - $archToken = Get-NvmReleaseArchToken -Architecture $Architecture - return ("nvm-{0}-{1}-sync.exe" -f $Version, $archToken) -} - -function Add-NvmGitHubJobSummary { - param( - [Parameter(Mandatory = $true)] - [string]$Markdown - ) - - if ([string]::IsNullOrWhiteSpace($env:GITHUB_STEP_SUMMARY)) { - return - } - Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value $Markdown -Encoding utf8 -} - -function Write-NvmJobSummaryTable { - param( - [Parameter(Mandatory = $true)] - [string]$Title, - [AllowEmptyCollection()] - [object[]]$Rows = @(), - [string]$EmptyMessage = "_None._", - [string]$Note = "" - ) - - $lines = New-Object System.Collections.Generic.List[string] - $lines.Add("## $Title") - $lines.Add("") - if (-not [string]::IsNullOrWhiteSpace($Note)) { - $lines.Add($Note) - $lines.Add("") - } - - if ($null -eq $Rows -or $Rows.Count -eq 0) { - $lines.Add($EmptyMessage) - $lines.Add("") - Add-NvmGitHubJobSummary -Markdown ($lines -join "`n") - return - } - - $lines.Add("| Status | Size | Path |") - $lines.Add("| --- | --- | --- |") - foreach ($row in $Rows) { - $status = [string]$row.Status - $size = [string]$row.Size - $path = if ($row.PSObject.Properties["Display"] -and $row.Display) { [string]$row.Display } else { [string]$row.Output } - $path = $path.Replace("|", "\|") - $lines.Add(("| {0} | {1} | ``{2}`` |" -f $status, $size, $path)) - } - $lines.Add("") - Add-NvmGitHubJobSummary -Markdown ($lines -join "`n") -} - -function Write-NvmPayloadSummary { - param( - [Parameter(Mandatory = $true)] - [string[]]$Paths, - [string]$Title = "Payload summary", - [string]$DisplayRoot = "", - [switch]$SkipThrowOnMissing, - [switch]$SkipJobSummary - ) - - $rootFull = "" - if (-not [string]::IsNullOrWhiteSpace($DisplayRoot)) { - $rootFull = [System.IO.Path]::GetFullPath($DisplayRoot).TrimEnd('\', '/') - } - - $rows = foreach ($path in $Paths) { - $full = [System.IO.Path]::GetFullPath($path) - $exists = Test-Path -LiteralPath $full -PathType Leaf - $size = "" - if ($exists) { - $size = Format-NvmByteSize -Bytes (Get-Item -LiteralPath $full).Length - } - $display = $full - if ($rootFull -and $full.StartsWith($rootFull, [System.StringComparison]::OrdinalIgnoreCase)) { - $rel = $full.Substring($rootFull.Length).TrimStart('\', '/') - if (-not [string]::IsNullOrWhiteSpace($rel)) { - $display = $rel -replace '\\', '/' - } - } - [pscustomobject]@{ - Status = if ($exists) { "OK" } else { "MISSING" } - Size = $size - Output = $full - Display = $display - } - } - - Write-Host "" - Write-Host $Title - Write-Host ("=" * [Math]::Max(3, $Title.Length)) - $rows | Format-Table -AutoSize Status, Size, @{ Label = "Output"; Expression = { $_.Display } } - - if (-not $SkipJobSummary) { - Write-NvmJobSummaryTable -Title $Title -Rows $rows - } - - if (-not $SkipThrowOnMissing) { - $missing = @($rows | Where-Object { $_.Status -eq "MISSING" }) - if ($missing.Count -gt 0) { - throw ("Missing {0} expected payload path(s)." -f $missing.Count) - } - } -} - -function Write-NvmBuildJobSummaryHeader { - param( - [string]$Version = "", - [string]$Architecture = "", - [string]$ZigVersion = "" - ) - - if ([string]::IsNullOrWhiteSpace($env:GITHUB_STEP_SUMMARY)) { - return - } - - $lines = New-Object System.Collections.Generic.List[string] - $lines.Add("# Community build") - $lines.Add("") - $lines.Add("| | |") - $lines.Add("| --- | --- |") - if (-not [string]::IsNullOrWhiteSpace($Version)) { - $lines.Add("| Version | $Version |") - } - if (-not [string]::IsNullOrWhiteSpace($Architecture)) { - $lines.Add("| Architecture | $Architecture |") - } - if (-not [string]::IsNullOrWhiteSpace($ZigVersion)) { - $lines.Add("| Zig | $ZigVersion |") - } - $lines.Add("| Signing | Authenticode (Artifact Signing) |") - $lines.Add("") - Add-NvmGitHubJobSummary -Markdown ($lines -join "`n") -} - -function Resolve-NvmHostArchitecture { - if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - return "arm64" - } - return "amd64" -} diff --git a/build/main.ps1 b/build/main.ps1 deleted file mode 100644 index 88a7dc4a..00000000 --- a/build/main.ps1 +++ /dev/null @@ -1,152 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "", - [ValidateSet("All", "Cli", "Shims", "Sync")] - [string]$Component = "All", - [string]$BinRoot = "", - [switch]$SkipInstaller, - [switch]$DownloadSync, - [string]$SyncReleaseTag = "", - [string]$SyncReleaseRepo = "nvm-windows/nvm", - [string]$Hotfix = "", - [string]$Version = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "common.ps1") - -if ([string]::IsNullOrWhiteSpace($Architecture)) { - $Architecture = Resolve-NvmHostArchitecture -} - -# Read base from on-disk manifest only (ignore leftover NVM_CLI_VERSION). -$manifestPath = Get-NvmCliManifestPath -if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "CLI manifest not found: $manifestPath" -} -$base = [string](Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json).version -if ([string]::IsNullOrWhiteSpace($base)) { - throw "CLI manifest does not define version: $manifestPath" -} - -$hotfixSet = -not [string]::IsNullOrWhiteSpace($Hotfix) -$versionSet = -not [string]::IsNullOrWhiteSpace($Version) -if ($hotfixSet -and $versionSet) { - throw "Hotfix and Version are mutually exclusive; set only one." -} - -if ($versionSet) { - $effective = $Version.Trim() -} -elseif ($hotfixSet) { - $effective = Resolve-NvmHotfixVersion -BaseVersion $base -Hotfix $Hotfix -} -else { - $effective = $base -} - -$priorEnvVersion = [Environment]::GetEnvironmentVariable("NVM_CLI_VERSION") -$manifestStamped = $false -if ($effective -ne $base) { - Set-NvmCliManifestVersion -Version $effective - $manifestStamped = $true - if ($versionSet) { - Write-Host ("Version override -> {0} (base {1})" -f $effective, $base) - } - else { - Write-Host ("Prerelease stamp -> {0} (base {1})" -f $effective, $base) - } -} -$env:NVM_CLI_VERSION = $effective - -try { - $ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - $stepRoot = Join-Path $PSScriptRoot "steps" - $commonArgs = @{ - Architecture = $Architecture - BinRoot = $ctx.BinRoot - } - $syncArgs = @{ - Architecture = $Architecture - BinRoot = $ctx.BinRoot - SyncReleaseTag = $SyncReleaseTag - SyncReleaseRepo = $SyncReleaseRepo - } - if ($DownloadSync) { - $syncArgs["DownloadSync"] = $true - } - - Write-Host "Community build" - Write-Host " RepoRoot -> $($ctx.RepoRoot)" - Write-Host " BinRoot -> $($ctx.BinRoot)" - Write-Host " Architecture -> $Architecture" - Write-Host " Component -> $Component" - Write-Host " SkipInstaller -> $SkipInstaller" - Write-Host " DownloadSync -> $DownloadSync" - if ($DownloadSync) { - Write-Host " SyncReleaseTag -> $(if ([string]::IsNullOrWhiteSpace($SyncReleaseTag)) { '(from CLI manifest)' } else { $SyncReleaseTag })" - Write-Host " SyncReleaseRepo -> $SyncReleaseRepo" - } - if ($hotfixSet) { - Write-Host " Prerelease -> $Hotfix" - } - if ($versionSet) { - Write-Host " Version -> $Version" - } - Write-Host " CLI version -> $($ctx.CliVersion)" - Write-Host " Signing -> Authenticode via Artifact Signing (exes + NVMWindows.Events.dll)" - - switch ($Component) { - "All" { - & (Join-Path $stepRoot "Build-Cli.ps1") @commonArgs - & (Join-Path $stepRoot "Build-EventProvider.ps1") @commonArgs - & (Join-Path $stepRoot "Build-Shims.ps1") @commonArgs - & (Join-Path $stepRoot "Build-Sync.ps1") @syncArgs - } - "Cli" { - & (Join-Path $stepRoot "Build-Cli.ps1") @commonArgs - & (Join-Path $stepRoot "Build-EventProvider.ps1") @commonArgs - } - "Shims" { - & (Join-Path $stepRoot "Build-Shims.ps1") @commonArgs - } - "Sync" { - & (Join-Path $stepRoot "Build-Sync.ps1") @syncArgs - } - } - - $expected = Get-NvmExpectedExePaths -BinRoot $ctx.BinRoot -Component $Component - Write-NvmPayloadSummary -Paths $expected -Title "Executables" -DisplayRoot $ctx.BinRoot -SkipJobSummary - - if ($Component -eq "All" -or $Component -eq "Cli") { - $eventAssets = Get-NvmExpectedEventProviderPaths -BinRoot $ctx.BinRoot - Write-NvmPayloadSummary -Paths $eventAssets -Title "Event provider" -DisplayRoot $ctx.BinRoot -SkipJobSummary - } - - if ($Component -ne "All") { - Write-Host "Skipping Inno Setup (requires -Component All; got $Component)." -ForegroundColor Yellow - } - elseif ($SkipInstaller) { - Write-Host "Skipping Inno Setup (-SkipInstaller)." -ForegroundColor Yellow - } - else { - & (Join-Path $stepRoot "Build-Installer.ps1") @commonArgs - $setup = Get-NvmInstallerSetupPath -Version $ctx.CliVersion -Architecture $Architecture -DistRoot $ctx.DistRoot - Write-NvmPayloadSummary -Paths @($setup) -Title "Installer" -DisplayRoot $ctx.RepoRoot - } - - Write-Host "Build complete ($Component)." -} -finally { - if ($manifestStamped) { - Set-NvmCliManifestVersion -Version $base - } - if ($null -eq $priorEnvVersion -or $priorEnvVersion -eq "") { - Remove-Item Env:NVM_CLI_VERSION -ErrorAction SilentlyContinue - } - else { - $env:NVM_CLI_VERSION = $priorEnvVersion - } -} diff --git a/build/scripts/Ensure-ArtifactSigning.ps1 b/build/scripts/Ensure-ArtifactSigning.ps1 deleted file mode 100644 index e4c8c9e8..00000000 --- a/build/scripts/Ensure-ArtifactSigning.ps1 +++ /dev/null @@ -1,134 +0,0 @@ -# Ensures Azure.CodeSigning.Dlib.dll is available for community Authenticode signing. -# Adapted from certified/enhanced/scripts/Ensure-Dependencies.ps1 (-SigningOnly). - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -function Get-EnvOrEmpty { - param([string]$Name) - $value = [Environment]::GetEnvironmentVariable($Name) - if ($null -eq $value) { return "" } - return $value.Trim() -} - -function Get-ArtifactSigningToolsRoot { - $fromEnv = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_TOOLS_DIR" - if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { - return [System.IO.Path]::GetFullPath($fromEnv) - } - $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - return [System.IO.Path]::GetFullPath((Join-Path $repoRoot ".tools\artifact-signing")) -} - -function Get-ArtifactSigningClientVersion { - $fromEnv = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_CLIENT_VERSION" - if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { - return $fromEnv.Trim() - } - return "1.0.95" -} - -function Find-ArtifactSigningDlib { - $fromEnv = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_DLIB" - if ([string]::IsNullOrWhiteSpace($fromEnv)) { - $fromEnv = Get-EnvOrEmpty -Name "TRUSTED_SIGNING_DLIB" - } - if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { - $full = [System.IO.Path]::GetFullPath($fromEnv) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw "ARTIFACT_SIGNING_DLIB path missing: $full" - } - return $full - } - - $toolsRoot = Get-ArtifactSigningToolsRoot - $known = @( - (Join-Path $toolsRoot "bin\x64\Azure.CodeSigning.Dlib.dll"), - (Join-Path $toolsRoot "Azure.CodeSigning.Dlib.dll"), - (Join-Path $toolsRoot "x64\Azure.CodeSigning.Dlib.dll"), - (Join-Path $env:LOCALAPPDATA "Microsoft\MicrosoftArtifactSigningClientTools\Azure.CodeSigning.Dlib.dll"), - (Join-Path $env:LOCALAPPDATA "Microsoft\MicrosoftArtifactSigningClientTools\x64\Azure.CodeSigning.Dlib.dll") - ) - foreach ($candidate in $known) { - if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { - return $candidate - } - } - - if (Test-Path -LiteralPath $toolsRoot -PathType Container) { - $hit = Get-ChildItem -LiteralPath $toolsRoot -Recurse -Filter "Azure.CodeSigning.Dlib.dll" -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match '(?i)[\\/]x64[\\/]' } | - Select-Object -First 1 - if ($null -ne $hit) { return $hit.FullName } - $hit = Get-ChildItem -LiteralPath $toolsRoot -Recurse -Filter "Azure.CodeSigning.Dlib.dll" -ErrorAction SilentlyContinue | - Select-Object -First 1 - if ($null -ne $hit) { return $hit.FullName } - } - return $null -} - -function Install-ArtifactSigningClientTools { - $toolsRoot = Get-ArtifactSigningToolsRoot - $version = Get-ArtifactSigningClientVersion - New-Item -ItemType Directory -Force -Path $toolsRoot | Out-Null - - $existing = Find-ArtifactSigningDlib - if (-not [string]::IsNullOrWhiteSpace($existing) -and $existing.StartsWith($toolsRoot, [StringComparison]::OrdinalIgnoreCase)) { - Write-Host "Artifact Signing client already present -> $existing" - return - } - - $nupkgPath = Join-Path $toolsRoot "client.nupkg" - $extractRoot = Join-Path $toolsRoot "pkg" - $url = "https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client/$version" - Write-Host "Downloading Microsoft.Trusted.Signing.Client $version..." - $prev = $ProgressPreference - $ProgressPreference = "SilentlyContinue" - try { - Invoke-WebRequest -Uri $url -OutFile $nupkgPath -UseBasicParsing - } - finally { - $ProgressPreference = $prev - } - - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null - $zipPath = Join-Path $toolsRoot "client.zip" - Copy-Item -LiteralPath $nupkgPath -Destination $zipPath -Force - Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force - - $dlib = Get-ChildItem -LiteralPath $extractRoot -Recurse -Filter "Azure.CodeSigning.Dlib.dll" -File -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match '(?i)[\\/](bin[\\/])?x64[\\/]' } | - Select-Object -First 1 - if ($null -eq $dlib) { - $dlib = Get-ChildItem -LiteralPath $extractRoot -Recurse -Filter "Azure.CodeSigning.Dlib.dll" -File -ErrorAction SilentlyContinue | - Select-Object -First 1 - } - if ($null -eq $dlib) { - throw "Azure.CodeSigning.Dlib.dll not found in NuGet package" - } - - $binOut = Join-Path $toolsRoot "bin\x64" - New-Item -ItemType Directory -Force -Path $binOut | Out-Null - Copy-Item -LiteralPath $dlib.FullName -Destination (Join-Path $binOut "Azure.CodeSigning.Dlib.dll") -Force - Get-ChildItem -LiteralPath $dlib.Directory.FullName -File -ErrorAction SilentlyContinue | - Where-Object { $_.Name -ne "Azure.CodeSigning.Dlib.dll" } | - ForEach-Object { Copy-Item $_.FullName (Join-Path $binOut $_.Name) -Force } - - $env:ARTIFACT_SIGNING_DLIB = Join-Path $binOut "Azure.CodeSigning.Dlib.dll" - Write-Host "Staged Artifact Signing dlib -> $($env:ARTIFACT_SIGNING_DLIB)" - Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue -} - -$found = Find-ArtifactSigningDlib -if ([string]::IsNullOrWhiteSpace($found)) { - Install-ArtifactSigningClientTools - $found = Find-ArtifactSigningDlib -} -if ([string]::IsNullOrWhiteSpace($found)) { - throw "Artifact Signing dlib still missing after install" -} -$env:ARTIFACT_SIGNING_DLIB = $found -Write-Host "ARTIFACT_SIGNING_DLIB -> $found" diff --git a/build/scripts/Sign-Executables.ps1 b/build/scripts/Sign-Executables.ps1 deleted file mode 100644 index da50c3a9..00000000 --- a/build/scripts/Sign-Executables.ps1 +++ /dev/null @@ -1,324 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string[]]$Path, - - [string]$Endpoint = "", - [string]$AccountName = "", - [string]$CertificateProfileName = "", - [string]$TimestampUrl = "http://timestamp.acs.microsoft.com", - [string]$SignToolPath = "", - [string]$DlibPath = "", - [string]$MetadataPath = "", - [string]$CorrelationId = "", - # default | environment | azure-cli - [string]$AuthMode = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -function Get-EnvOrEmpty { - param([string]$Name) - $value = [Environment]::GetEnvironmentVariable($Name) - if ($null -eq $value) { - return "" - } - return $value.Trim() -} - -function Resolve-SignToolPath { - param([string]$ExplicitPath) - - if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { - $full = [System.IO.Path]::GetFullPath($ExplicitPath) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw "SignTool not found at $full" - } - return $full - } - - $fromPath = Get-Command signtool -ErrorAction SilentlyContinue - if ($null -ne $fromPath -and -not [string]::IsNullOrWhiteSpace([string]$fromPath.Source)) { - return [string]$fromPath.Source - } - - $kitRoot = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin" - if (-not (Test-Path -LiteralPath $kitRoot -PathType Container)) { - throw "SignTool not found on PATH and Windows Kits bin root missing: $kitRoot" - } - - $candidate = Get-ChildItem -LiteralPath $kitRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | - Sort-Object { [version]$_.Name } -Descending | - ForEach-Object { Join-Path $_.FullName "x64\signtool.exe" } | - Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | - Select-Object -First 1 - - if ([string]::IsNullOrWhiteSpace([string]$candidate)) { - throw "Unable to locate x64 signtool.exe under $kitRoot" - } - return [string]$candidate -} - -function Resolve-ArtifactSigningDlibPath { - param([string]$ExplicitPath) - - if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { - $full = [System.IO.Path]::GetFullPath($ExplicitPath) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw "Artifact Signing dlib not found at $full" - } - return $full - } - - $fromEnv = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_DLIB" - if ([string]::IsNullOrWhiteSpace($fromEnv)) { - $fromEnv = Get-EnvOrEmpty -Name "TRUSTED_SIGNING_DLIB" - } - if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { - $full = [System.IO.Path]::GetFullPath($fromEnv) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw "Artifact Signing dlib from env not found: $full" - } - return $full - } - - $searchRoots = @( - ${env:ProgramFiles}, - ${env:ProgramFiles(x86)}, - (Join-Path $env:LOCALAPPDATA "Microsoft") - ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and (Test-Path -LiteralPath $_ -PathType Container) } - - $candidates = foreach ($root in $searchRoots) { - Get-ChildItem -LiteralPath $root -Recurse -Filter "Azure.CodeSigning.Dlib.dll" -ErrorAction SilentlyContinue - } - if ($null -eq $candidates) { - throw @" -Artifact Signing dlib (Azure.CodeSigning.Dlib.dll) not found. - -Install client tools: - winget install -e --id Microsoft.Azure.ArtifactSigningClientTools - -Or set ARTIFACT_SIGNING_DLIB to Azure.CodeSigning.Dlib.dll path. -"@ - } - - $preferred = @($candidates | Where-Object { $_.FullName -match '(?i)[\\/]x64[\\/]' } | Select-Object -First 1) - if ($preferred.Count -gt 0) { - return $preferred[0].FullName - } - return (@($candidates | Select-Object -First 1))[0].FullName -} - -function Get-ExcludeCredentialsForAuthMode { - param([string]$Mode) - - $normalized = $Mode.Trim().ToLowerInvariant() - switch ($normalized) { - "" { return @() } - "default" { return @() } - "environment" { - return @( - "ManagedIdentityCredential", - "WorkloadIdentityCredential", - "SharedTokenCacheCredential", - "VisualStudioCredential", - "VisualStudioCodeCredential", - "AzureCliCredential", - "AzurePowerShellCredential", - "AzureDeveloperCliCredential", - "InteractiveBrowserCredential" - ) - } - "azure-cli" { - return @( - "ManagedIdentityCredential", - "EnvironmentCredential", - "WorkloadIdentityCredential", - "SharedTokenCacheCredential", - "VisualStudioCredential", - "VisualStudioCodeCredential", - "AzurePowerShellCredential", - "AzureDeveloperCliCredential", - "InteractiveBrowserCredential" - ) - } - default { - throw "Unsupported AuthMode '$Mode'. Use default, environment, or azure-cli." - } - } -} - -function Format-SignToolFailureHint { - param([string]$LogText) - - $hints = New-Object System.Collections.Generic.List[string] - if ($LogText -match 'Status:\s*403') { - $hints.Add("Azure returned 403 Forbidden. Grant the signing identity 'Artifact Signing Certificate Profile Signer' on account '$AccountName' (profile '$CertificateProfileName').") - $hints.Add("If AZURE_CLIENT_ID/SECRET are set, dlib uses that service principal first β€” role must be on the SP, not only your user.") - $hints.Add("Confirm Endpoint region matches the Artifact Signing account region.") - } - if ($LogText -match 'AADSTS50173|grant has expired|TokensValidFrom') { - $hints.Add("Azure CLI grant expired/revoked. Re-auth for Artifact Signing scope:") - $hints.Add(' az logout') - $hints.Add(' az login --scope "https://codesigning.azure.net/.default"') - $hints.Add("Then set ARTIFACT_SIGNING_AUTH=azure-cli (or -AuthMode azure-cli) so SP env vars are ignored.") - } - if ($LogText -match 'CredentialUnavailableException' -and $LogText -match 'AzureCliCredential') { - $hints.Add("AzureCliCredential unavailable. Run az login with codesigning scope, or use AuthMode=environment with a correctly permissioned SP.") - } - return ($hints -join [Environment]::NewLine) -} - -if ([string]::IsNullOrWhiteSpace($Endpoint)) { - $Endpoint = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_ENDPOINT" - if ([string]::IsNullOrWhiteSpace($Endpoint)) { - $Endpoint = Get-EnvOrEmpty -Name "TRUSTED_SIGNING_ENDPOINT" - } -} -if ([string]::IsNullOrWhiteSpace($AccountName)) { - $AccountName = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_ACCOUNT" - if ([string]::IsNullOrWhiteSpace($AccountName)) { - $AccountName = Get-EnvOrEmpty -Name "TRUSTED_SIGNING_ACCOUNT" - } -} -if ([string]::IsNullOrWhiteSpace($CertificateProfileName)) { - $CertificateProfileName = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_CERTIFICATE_PROFILE" - if ([string]::IsNullOrWhiteSpace($CertificateProfileName)) { - $CertificateProfileName = Get-EnvOrEmpty -Name "TRUSTED_SIGNING_CERTIFICATE_PROFILE" - } -} -if ([string]::IsNullOrWhiteSpace($CorrelationId)) { - $CorrelationId = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_CORRELATION_ID" -} -if ([string]::IsNullOrWhiteSpace($AuthMode)) { - $AuthMode = Get-EnvOrEmpty -Name "ARTIFACT_SIGNING_AUTH" - if ([string]::IsNullOrWhiteSpace($AuthMode)) { - $AuthMode = "default" - } -} - -if ([string]::IsNullOrWhiteSpace($Endpoint) -or [string]::IsNullOrWhiteSpace($AccountName) -or [string]::IsNullOrWhiteSpace($CertificateProfileName)) { - throw @" -Artifact Signing configuration incomplete. - -Provide -Endpoint / -AccountName / -CertificateProfileName, or set: - ARTIFACT_SIGNING_ENDPOINT - ARTIFACT_SIGNING_ACCOUNT - ARTIFACT_SIGNING_CERTIFICATE_PROFILE - -(Legacy TRUSTED_SIGNING_* env names also accepted.) -"@ -} - -$signTool = Resolve-SignToolPath -ExplicitPath $SignToolPath -$dlib = Resolve-ArtifactSigningDlibPath -ExplicitPath $DlibPath -$excludeCredentials = @(Get-ExcludeCredentialsForAuthMode -Mode $AuthMode) - -$metadataFile = $MetadataPath -$tempMetadata = $false -if ([string]::IsNullOrWhiteSpace($metadataFile)) { - $metadataFile = Join-Path ([System.IO.Path]::GetTempPath()) ("artifact-signing-{0}.json" -f [guid]::NewGuid().ToString("N")) - $tempMetadata = $true -} - -$metadata = [ordered]@{ - Endpoint = $Endpoint.TrimEnd("/") - CodeSigningAccountName = $AccountName - CertificateProfileName = $CertificateProfileName -} -if (-not [string]::IsNullOrWhiteSpace($CorrelationId)) { - $metadata["CorrelationId"] = $CorrelationId -} -if ($excludeCredentials.Count -gt 0) { - $metadata["ExcludeCredentials"] = $excludeCredentials -} - -$metadataJson = ($metadata | ConvertTo-Json -Depth 5) -[System.IO.File]::WriteAllText($metadataFile, $metadataJson, [System.Text.UTF8Encoding]::new($false)) - -$files = New-Object System.Collections.Generic.List[string] -foreach ($item in $Path) { - if ([string]::IsNullOrWhiteSpace($item)) { - continue - } - $full = [System.IO.Path]::GetFullPath($item) - if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { - throw "File not found for signing: $full" - } - $ext = [System.IO.Path]::GetExtension($full).ToLowerInvariant() - $name = [System.IO.Path]::GetFileName($full) - # Authenticode: .exe product binaries, .msi packages, and the ETW message - # resource DLL (wevtutil). Worker .dll = COSE Sign1 (Sign-SyncWorkers). - # .intunewin = container around signed MSI (not Authenticode). - $allowDll = ($ext -eq ".dll") -and ($name -ieq "NVMWindows.Events.dll") - if ($ext -ne ".exe" -and $ext -ne ".msi" -and -not $allowDll) { - throw "Only .exe/.msi/NVMWindows.Events.dll signed here (worker .dll COSE = Sign-SyncWorkers.ps1): $full" - } - $files.Add($full) -} - -if ($files.Count -eq 0) { - throw "No .exe/.msi/NVMWindows.Events.dll files provided for Artifact Signing." -} - -$spEnvSet = -not [string]::IsNullOrWhiteSpace((Get-EnvOrEmpty -Name "AZURE_CLIENT_ID")) - -Write-Host "Artifact Signing via SignTool" -Write-Host " SignTool -> $signTool" -Write-Host " Dlib -> $dlib" -Write-Host " Endpoint -> $($metadata.Endpoint)" -Write-Host " Account -> $AccountName" -Write-Host " Profile -> $CertificateProfileName" -Write-Host " AuthMode -> $AuthMode" -Write-Host (" SP env -> {0}" -f $(if ($spEnvSet) { "AZURE_CLIENT_ID set" } else { "not set" })) -Write-Host (" Files -> {0}" -f $files.Count) - -try { - foreach ($file in $files) { - Write-Host "Signing -> $file" - $logFile = Join-Path ([System.IO.Path]::GetTempPath()) ("signtool-{0}.log" -f [guid]::NewGuid().ToString("N")) - try { - & $signTool sign ` - /v ` - /debug ` - /fd SHA256 ` - /tr $TimestampUrl ` - /td SHA256 ` - /dlib $dlib ` - /dmdf $metadataFile ` - $file *>&1 | - Tee-Object -FilePath $logFile | - ForEach-Object { Write-Host "$_" } - - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - $logText = "" - if (Test-Path -LiteralPath $logFile -PathType Leaf) { - $logText = Get-Content -LiteralPath $logFile -Raw -ErrorAction SilentlyContinue - } - $hint = Format-SignToolFailureHint -LogText $logText - $msg = "SignTool failed for $file with exit code $LASTEXITCODE" - if (-not [string]::IsNullOrWhiteSpace($hint)) { - $msg = "$msg`n$hint" - } - throw $msg - } - - $signature = Get-AuthenticodeSignature -LiteralPath $file - if ($signature.Status -ne "Valid") { - throw ("Authenticode status for {0} is {1}: {2}" -f $file, $signature.Status, $signature.StatusMessage) - } - Write-Host ("Signed OK -> {0} ({1})" -f $file, $signature.SignerCertificate.Subject) - } - finally { - Remove-Item -LiteralPath $logFile -Force -ErrorAction SilentlyContinue - } - } -} -finally { - if ($tempMetadata -and (Test-Path -LiteralPath $metadataFile -PathType Leaf)) { - Remove-Item -LiteralPath $metadataFile -Force -ErrorAction SilentlyContinue - } -} - -return $files.ToArray() diff --git a/build/steps/Build-Cli.ps1 b/build/steps/Build-Cli.ps1 deleted file mode 100644 index 917565d3..00000000 --- a/build/steps/Build-Cli.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -$cliSrc = Join-Path $ctx.RepoRoot "cli\src" -$defaultNvm = Join-Path (Get-NvmDefaultBinRoot) "nvm.exe" -$targetNvm = Join-Path $ctx.BinRoot "nvm.exe" - -if (-not (Test-Path -LiteralPath $cliSrc -PathType Container)) { - throw "CLI source not found: $cliSrc" -} - -function Ensure-GoWinres { - $cmd = Get-Command go-winres -ErrorAction SilentlyContinue - if ($null -ne $cmd) { - Write-Host "go-winres -> $($cmd.Source)" - return - } - Write-Host "Installing go-winres..." - & go install github.com/tc-hib/go-winres@latest - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "go install go-winres failed with exit code $LASTEXITCODE" - } - $cmd = Get-Command go-winres -ErrorAction SilentlyContinue - if ($null -eq $cmd) { - throw "go-winres not on PATH after install (ensure GOBIN/GOPATH/bin is on PATH)" - } - Write-Host "go-winres -> $($cmd.Source)" -} - -$originalGoos = $env:GOOS -$originalGoarch = $env:GOARCH -$originalGowork = $env:GOWORK -$pushed = $false - -Write-Host "Building CLI -> $targetNvm" -try { - Ensure-GoWinres - $env:GOWORK = "off" - $env:GOOS = "windows" - $env:GOARCH = if ($Architecture -eq "arm64") { "arm64" } else { "amd64" } - - Push-Location $cliSrc - $pushed = $true - - if (Test-Path -LiteralPath (Join-Path $cliSrc "winres\winres.json") -PathType Leaf) { - & qgo exec prebuild - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "CLI prebuild failed with exit code $LASTEXITCODE" - } - } - - & qgo build --no-cache - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "CLI build failed with exit code $LASTEXITCODE" - } -} -finally { - if ($pushed) { - Pop-Location - } - $env:GOOS = $originalGoos - $env:GOARCH = $originalGoarch - $env:GOWORK = $originalGowork -} - -Assert-NvmFile -Path $defaultNvm -Label "nvm.exe" - -if ([System.IO.Path]::GetFullPath($defaultNvm) -ne [System.IO.Path]::GetFullPath($targetNvm)) { - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $targetNvm) | Out-Null - Copy-Item -LiteralPath $defaultNvm -Destination $targetNvm -Force - Write-Host "Copied nvm.exe -> $targetNvm" -} - -Assert-NvmFile -Path $targetNvm -Label "nvm.exe" -Write-Host "CLI ready -> $targetNvm" -return $targetNvm diff --git a/build/steps/Build-EventProvider.ps1 b/build/steps/Build-EventProvider.ps1 deleted file mode 100644 index 85ffe881..00000000 --- a/build/steps/Build-EventProvider.ps1 +++ /dev/null @@ -1,179 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "" -) - -# Build NVMWindows.Events.man + resource DLL next to nvm.exe. -# Required by common/eventlog.RegisterEventSource (wevtutil im /rf /mf /pf). -# Message resource DLL is Authenticode-signed with product .exe/.msi (not COSE; -# COSE is for sync workers only). - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -function Invoke-NvmTemplateRender { - param( - [Parameter(Mandatory = $true)][string]$TemplatePath, - [Parameter(Mandatory = $true)][string]$OutputPath, - [Parameter(Mandatory = $true)][hashtable]$Tokens - ) - - $content = Get-Content -LiteralPath $TemplatePath -Raw - foreach ($entry in $Tokens.GetEnumerator()) { - $content = $content.Replace($entry.Key, [string]$entry.Value) - } - $encoding = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::WriteAllText([System.IO.Path]::GetFullPath($OutputPath), $content, $encoding) -} - -function Resolve-WindowsSdkTool { - param( - [Parameter(Mandatory = $true)][string]$ToolName - ) - - $sdkBin = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin" - if (-not (Test-Path -LiteralPath $sdkBin -PathType Container)) { - throw "Windows SDK bin root not found: $sdkBin" - } - - $sdkVer = Get-ChildItem -LiteralPath $sdkBin -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | - Sort-Object { [version]$_.Name } -Descending | - Select-Object -First 1 -ExpandProperty Name - - if ([string]::IsNullOrWhiteSpace($sdkVer)) { - throw "No Windows SDK version directory under $sdkBin" - } - - $path = Join-Path $sdkBin "$sdkVer\x64\$ToolName" - if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { - throw "$ToolName not found: $path" - } - return $path -} - -function Resolve-LinkExe { - param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture - ) - - $vsRoots = @( - (Join-Path ${env:ProgramFiles} "Microsoft Visual Studio"), - (Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio") - ) | Where-Object { Test-Path -LiteralPath $_ -PathType Container } - - $hostArch = if ($Architecture -eq "arm64") { "arm64" } else { "x64" } - $patterns = @( - "Hostx64\\$hostArch", - "Hostarm64\\$hostArch", - "Hostx86\\$hostArch" - ) - - foreach ($root in $vsRoots) { - $candidates = Get-ChildItem -LiteralPath $root -Recurse -Filter "link.exe" -ErrorAction SilentlyContinue | - Where-Object { - $full = $_.FullName - foreach ($p in $patterns) { - if ($full -match $p) { return $true } - } - return $false - } | - Select-Object -ExpandProperty FullName - - $pick = @($candidates) | Select-Object -First 1 - if (-not [string]::IsNullOrWhiteSpace($pick)) { - return $pick - } - } - - throw "link.exe not found under Visual Studio (need Host*\$hostArch for $Architecture)" -} - -$installerRoot = Join-Path $ctx.RepoRoot "installer" -$templatePath = Join-Path $installerRoot "NVMWindows.Events.man.template" -if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) { - throw "Event provider template missing: $templatePath" -} - -$manifestPath = Get-NvmCliManifestPath -$cliManifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json - -$eventProviderDisplayName = [string]$cliManifest.appLabel -if ([string]::IsNullOrWhiteSpace($eventProviderDisplayName)) { - throw "manifest.json missing appLabel for event provider display name" -} - -$eventProviderManifestFileName = [string]$cliManifest.eventLabel -if ([string]::IsNullOrWhiteSpace($eventProviderManifestFileName)) { - throw "manifest.json missing eventLabel for event provider manifest filename" -} - -$eventProviderChannelLeafName = "Operational" -$eventProviderName = $eventProviderDisplayName -$eventProviderChannelPrefix = $eventProviderDisplayName -$eventProviderChannelName = "$eventProviderChannelPrefix/$eventProviderChannelLeafName" -$eventResourceDllFileName = [System.IO.Path]::ChangeExtension($eventProviderManifestFileName, ".dll") - -$eventManifestOut = Join-Path $ctx.BinRoot $eventProviderManifestFileName -$eventResourceDllOut = Join-Path $ctx.BinRoot $eventResourceDllFileName - -$tokens = @{ - "__EVENT_PROVIDER_NAME__" = $eventProviderName - "__EVENT_PROVIDER_DISPLAY_NAME__" = $eventProviderDisplayName - "__EVENT_PROVIDER_CHANNEL_DISPLAY_NAME__" = $eventProviderChannelLeafName - "__EVENT_PROVIDER_ORGANIZATION__" = $eventProviderDisplayName - "__EVENT_PROVIDER_CHANNEL_PREFIX__" = $eventProviderChannelPrefix - "__EVENT_PROVIDER_CHANNEL_NAME__" = $eventProviderChannelName - "__EVENT_PROVIDER_MANIFEST_FILE_NAME__" = $eventProviderManifestFileName - "__EVENT_RESOURCE_DLL_NAME__" = $eventResourceDllFileName - "__EVENT_RESOURCE_DLL_FILE_NAME__" = $eventResourceDllFileName -} - -Write-Host "Building event provider assets ($Architecture)..." -Invoke-NvmTemplateRender -TemplatePath $templatePath -OutputPath $eventManifestOut -Tokens $tokens - -$mcExePath = Resolve-WindowsSdkTool -ToolName "mc.exe" -$rcExePath = Resolve-WindowsSdkTool -ToolName "rc.exe" -$linkExePath = Resolve-LinkExe -Architecture $Architecture -$machine = if ($Architecture -eq "arm64") { "ARM64" } else { "X64" } - -$dllTempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-evtres-{0}" -f [guid]::NewGuid().ToString("N")) -New-Item -ItemType Directory -Path $dllTempDir -Force | Out-Null -try { - Push-Location $dllTempDir - & $mcExePath -z NVMWindowsEvents $eventManifestOut 2>&1 | ForEach-Object { Write-Host $_ } - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "mc.exe failed (exit $LASTEXITCODE) for $eventManifestOut" - } - & $rcExePath "NVMWindowsEvents.rc" 2>&1 | ForEach-Object { Write-Host $_ } - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "rc.exe failed (exit $LASTEXITCODE)" - } - & $linkExePath /DLL /NOENTRY "/MACHINE:$machine" "NVMWindowsEvents.res" "/OUT:$eventResourceDllOut" 2>&1 | - ForEach-Object { Write-Host $_ } - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "link.exe failed (exit $LASTEXITCODE)" - } -} -finally { - Pop-Location -ErrorAction SilentlyContinue - Remove-Item -LiteralPath $dllTempDir -Recurse -Force -ErrorAction SilentlyContinue -} - -Assert-NvmFile -Path $eventManifestOut -Label $eventProviderManifestFileName | Out-Null -Assert-NvmFile -Path $eventResourceDllOut -Label $eventResourceDllFileName | Out-Null -Write-Host "Event provider ready -> $eventManifestOut" -Write-Host "Event resource DLL -> $eventResourceDllOut" -# Authenticode via Sign-CommunityArtifacts / Sign-Executables (NVMWindows.Events.dll). -# COSE N/A (sync workers only). -Write-Host "Signing -> Authenticode with product binaries (NVMWindows.Events.dll)" - -return @{ - Manifest = $eventManifestOut - Dll = $eventResourceDllOut -} diff --git a/build/steps/Build-Installer.ps1 b/build/steps/Build-Installer.ps1 deleted file mode 100644 index 232294fd..00000000 --- a/build/steps/Build-Installer.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -$installerRoot = Join-Path $ctx.RepoRoot "installer" -$updaterScript = Join-Path $installerRoot "updater\build.ps1" -$bat = Join-Path $installerRoot "build-installer.bat" -$manifestPath = Get-NvmCliManifestPath - -if (-not (Test-Path -LiteralPath $updaterScript -PathType Leaf)) { - throw "Updater build script not found: $updaterScript" -} -if (-not (Test-Path -LiteralPath $bat -PathType Leaf)) { - throw "Inno build script not found: $bat" -} - -$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json -# Prefer gate/hotfix stamp (NVM_CLI_VERSION / patched manifest) over raw file alone. -$version = $ctx.CliVersion -$aumid = [string]$manifest.appUserModelId -$appId = [string]$manifest.appId -if ([string]::IsNullOrWhiteSpace($version) -or [string]::IsNullOrWhiteSpace($aumid) -or [string]::IsNullOrWhiteSpace($appId)) { - throw "installer manifest values (version, appUserModelId, appId) are required" -} - -Write-Host "Installer version -> $version" - -# Inno copies bin/.sync/* only when sync.exe exists; keep dir present, no DLLs. -New-Item -ItemType Directory -Force -Path (Join-Path $ctx.BinRoot ".sync") | Out-Null - -Write-Host "Building updater..." -& $updaterScript -Architecture $Architecture -if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "updater build failed with exit code $LASTEXITCODE" -} -Assert-NvmFile -Path (Join-Path $installerRoot "updater\bin\nvm-upgrader.exe") -Label "nvm-upgrader.exe" - -Write-Host "Building Inno Setup ($Architecture)..." -$prev = Get-Location -try { - Set-Location $installerRoot - & cmd /c ".\build-installer.bat $version $aumid $appId $Architecture" - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "build-installer.bat failed with exit code $LASTEXITCODE" - } -} -finally { - Set-Location $prev -} - -$setup = Get-NvmInstallerSetupPath -Version $version -Architecture $Architecture -DistRoot $ctx.DistRoot -$archToken = Get-NvmReleaseArchToken -Architecture $Architecture -$newest = Get-ChildItem -LiteralPath $ctx.DistRoot -Filter ("nvm-{0}-{1}-setup*.exe" -f $version, $archToken) -ErrorAction SilentlyContinue | - Sort-Object LastWriteTimeUtc -Descending | - Select-Object -First 1 -if ($null -ne $newest) { - if ($newest.FullName -ne $setup) { - Write-Warning ("Canonical setup was locked or stale. Newest build -> {0}" -f $newest.FullName) - try { - Copy-Item -LiteralPath $newest.FullName -Destination $setup -Force - Write-Host "Replaced canonical setup -> $setup" - } - catch { - Write-Warning ("Could not replace {0}. Run the timestamped file instead." -f $setup) - $setup = $newest.FullName - } - } -} -Assert-NvmFile -Path $setup -Label "Inno Setup installer" -Write-Host "Installer ready -> $setup" -return $setup diff --git a/build/steps/Build-Shims.ps1 b/build/steps/Build-Shims.ps1 deleted file mode 100644 index 2bdf3bb8..00000000 --- a/build/steps/Build-Shims.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -$shimBuildScript = Join-Path $ctx.RepoRoot "shim\build.ps1" -$rcEditBootstrapPath = Join-Path $ctx.RepoRoot "shim\scripts\.tools\rcedit-x64.exe" - -if (-not (Test-Path -LiteralPath $shimBuildScript -PathType Leaf)) { - throw "Shim build script not found: $shimBuildScript" -} - -# Community shims: no enhanced/zig registry override (HKCU only). -if ($env:RCEDIT_PATH) { - if (-not (Test-Path -LiteralPath $env:RCEDIT_PATH -PathType Leaf)) { - throw "RCEDIT_PATH is set but missing: $($env:RCEDIT_PATH)" - } -} -else { - $rcEditDir = Split-Path -Parent $rcEditBootstrapPath - New-Item -ItemType Directory -Force -Path $rcEditDir | Out-Null - if (-not (Test-Path -LiteralPath $rcEditBootstrapPath -PathType Leaf)) { - Write-Host "Downloading rcedit -> $rcEditBootstrapPath" - Invoke-WebRequest -Uri "https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe" -OutFile $rcEditBootstrapPath - } - $env:RCEDIT_PATH = $rcEditBootstrapPath -} - -Write-Host "Building shims -> $($ctx.BinRoot)" -& $shimBuildScript ` - -OutputDir $ctx.BinRoot ` - -Architecture $Architecture ` - -Version $ctx.CliVersion -if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "Shim build failed with exit code $LASTEXITCODE" -} - -$outputs = @( - (Assert-NvmFile -Path (Join-Path $ctx.BinRoot ".shim\node.exe") -Label "node.exe"), - (Assert-NvmFile -Path (Join-Path $ctx.BinRoot "utils\reshim.exe") -Label "reshim.exe"), - (Assert-NvmFile -Path (Join-Path $ctx.BinRoot "utils\proxy.exe") -Label "proxy.exe") -) - -Write-Host "Shims ready -> $($ctx.BinRoot)" -return $outputs diff --git a/build/steps/Build-Sync.ps1 b/build/steps/Build-Sync.ps1 deleted file mode 100644 index a255610b..00000000 --- a/build/steps/Build-Sync.ps1 +++ /dev/null @@ -1,232 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "", - [switch]$DownloadSync, - [string]$SyncReleaseTag = "", - [string]$SyncReleaseRepo = "nvm-windows/nvm" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -$syncRoot = Join-Path $ctx.RepoRoot "sync" -$syncSrcRoot = Join-Path $syncRoot "src" -$goModPath = Join-Path $syncSrcRoot "go.mod" -$defaultSyncExe = Join-Path (Get-NvmDefaultBinRoot) "utils\sync.exe" -$syncExePath = Join-Path $ctx.BinRoot "utils\sync.exe" -$hasSyncSource = (Test-Path -LiteralPath $syncSrcRoot -PathType Container) -and (Test-Path -LiteralPath $goModPath -PathType Leaf) - -New-Item -ItemType Directory -Force -Path (Split-Path -Parent $defaultSyncExe) | Out-Null -New-Item -ItemType Directory -Force -Path (Split-Path -Parent $syncExePath) | Out-Null -New-Item -ItemType Directory -Force -Path (Join-Path $ctx.BinRoot ".sync") | Out-Null - -function Ensure-GoWinres { - $cmd = Get-Command go-winres -ErrorAction SilentlyContinue - if ($null -ne $cmd) { - Write-Host "go-winres -> $($cmd.Source)" - return - } - Write-Host "Installing go-winres..." - & go install github.com/tc-hib/go-winres@latest - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "go install go-winres failed with exit code $LASTEXITCODE" - } - $cmd = Get-Command go-winres -ErrorAction SilentlyContinue - if ($null -eq $cmd) { - throw "go-winres not on PATH after install (ensure GOBIN/GOPATH/bin is on PATH)" - } - Write-Host "go-winres -> $($cmd.Source)" -} - -# Shared sync go.mod points mirrorauth/idp at certified/enhanced. Community tree -# has stub mirrorauth only β€” rewrite for this build, restore after. -function Use-CommunitySyncGoMod { - param([string]$Path) - - $utf8 = New-Object System.Text.UTF8Encoding $false - $raw = [System.IO.File]::ReadAllText($Path, $utf8) - $updated = $raw - $updated = $updated -replace '(replace\s+common/mirrorauth\s+v1\.0\.0\s+=>\s+)\S+', '${1}../../common/mirrorauth' - $updated = [regex]::Replace($updated, '(?m)^replace\s+common/idp\s+v1\.0\.0\s+=>\s+\S+\r?\n', '') - $updated = [regex]::Replace($updated, '(?m)^\tcommon/idp v1\.0\.0(?: // indirect)?\r?\n', '') - if ($updated -ne $raw) { - [System.IO.File]::WriteAllText($Path, $updated, $utf8) - Write-Host "Rewrote sync go.mod for community (stub mirrorauth, no enhanced/idp)" - } - return $raw -} - -function Get-NvmSyncReleaseTag { - param( - [string]$Tag, - [string]$Version - ) - $t = $Tag.Trim() - if (-not [string]::IsNullOrWhiteSpace($t)) { - if ($t.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { - return $t - } - return "v$t" - } - $v = $Version.Trim() - if ([string]::IsNullOrWhiteSpace($v)) { - throw "CLI version is empty; cannot resolve sync release tag" - } - if ($v.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { - return $v - } - return "v$v" -} - -function Install-NvmSyncFromRelease { - param( - [string]$Architecture, - [string]$Destination, - [string]$Repo, - [string]$Tag, - [string]$Version - ) - - $releaseTag = Get-NvmSyncReleaseTag -Tag $Tag -Version $Version - $assetVersion = $releaseTag.TrimStart("v", "V") - $assetName = Get-NvmSyncReleaseAssetName -Version $assetVersion -Architecture $Architecture - $uri = "https://github.com/{0}/releases/download/{1}/{2}" -f $Repo.Trim(), $releaseTag, $assetName - $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-sync-download-{0}-{1}.exe" -f $Architecture, [guid]::NewGuid().ToString("n")) - - Write-Host "Downloading prebuilt sync.exe" - Write-Host " Repo -> $Repo" - Write-Host " Tag -> $releaseTag" - Write-Host " Asset -> $assetName" - Write-Host " URL -> $uri" - - try { - Invoke-WebRequest -Uri $uri -OutFile $tmp -UseBasicParsing - } - catch { - throw @" -Failed to download sync.exe from $uri -$($_.Exception.Message) - -Use a published community release that includes nvm---sync.exe, -or pass -SyncReleaseTag / -SyncReleaseRepo. Sync source is private β€” compile only with maintainer access. -"@ - } - - if (-not (Test-Path -LiteralPath $tmp -PathType Leaf) -or ((Get-Item -LiteralPath $tmp).Length -le 0)) { - throw "Downloaded sync asset is missing or empty: $tmp" - } - - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Destination) | Out-Null - Copy-Item -LiteralPath $tmp -Destination $Destination -Force - Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue - Assert-NvmFile -Path $Destination -Label "sync.exe (downloaded)" - Write-Host "Sync ready (downloaded) -> $Destination" - return $Destination -} - -function Build-NvmSyncFromSource { - param( - [string]$Architecture, - [string]$SyncSrcRoot, - [string]$GoModPath, - [string]$DefaultSyncExe, - [string]$SyncExePath - ) - - $originalGoos = $env:GOOS - $originalGoarch = $env:GOARCH - $originalGowork = $env:GOWORK - $originalGoMod = $null - $srcPushed = $false - - Write-Host "Building sync.exe -> $SyncExePath (no worker DLLs)" - try { - Ensure-GoWinres - $originalGoMod = Use-CommunitySyncGoMod -Path $GoModPath - - $env:GOWORK = "off" - $env:GOOS = "windows" - $env:GOARCH = if ($Architecture -eq "arm64") { "arm64" } else { "amd64" } - - Push-Location $SyncSrcRoot - $srcPushed = $true - & qgo exec prebuild - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "sync src prebuild failed with exit code $LASTEXITCODE" - } - - & qgo build --no-cache - if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { - throw "sync src build failed with exit code $LASTEXITCODE" - } - } - finally { - if ($srcPushed) { - Pop-Location - } - if ($null -ne $originalGoMod) { - $utf8 = New-Object System.Text.UTF8Encoding $false - [System.IO.File]::WriteAllText($GoModPath, $originalGoMod, $utf8) - Write-Host "Restored sync go.mod" - } - $env:GOOS = $originalGoos - $env:GOARCH = $originalGoarch - $env:GOWORK = $originalGowork - } - - if (-not (Test-Path -LiteralPath $DefaultSyncExe -PathType Leaf)) { - throw "sync.exe was not generated at $DefaultSyncExe" - } - - if ([System.IO.Path]::GetFullPath($DefaultSyncExe) -ne [System.IO.Path]::GetFullPath($SyncExePath)) { - Copy-Item -LiteralPath $DefaultSyncExe -Destination $SyncExePath -Force - Write-Host "Copied sync.exe -> $SyncExePath" - } - - Assert-NvmFile -Path $SyncExePath -Label "sync.exe" - Write-Host "Sync ready -> $SyncExePath" - Write-Host "Worker DLLs skipped (published by certified release to assets.nvm-windows.com)." -ForegroundColor Yellow - return $SyncExePath -} - -if ($DownloadSync) { - Install-NvmSyncFromRelease ` - -Architecture $Architecture ` - -Destination $syncExePath ` - -Repo $SyncReleaseRepo ` - -Tag $SyncReleaseTag ` - -Version $ctx.CliVersion | Out-Null - - if ([System.IO.Path]::GetFullPath($defaultSyncExe) -ne [System.IO.Path]::GetFullPath($syncExePath)) { - Copy-Item -LiteralPath $syncExePath -Destination $defaultSyncExe -Force - } -} -elseif ($hasSyncSource) { - Build-NvmSyncFromSource ` - -Architecture $Architecture ` - -SyncSrcRoot $syncSrcRoot ` - -GoModPath $goModPath ` - -DefaultSyncExe $defaultSyncExe ` - -SyncExePath $syncExePath | Out-Null -} -else { - throw @" -Sync source not found: $syncSrcRoot - -The sync repository is private. Either: - - Pass -DownloadSync to fetch nvm---sync.exe from the GitHub Release, or - - Build with maintainer access to the sync submodule. -"@ -} - -# Stage release-named copy under .dist for GHA upload (compiled or downloaded). -$staged = Get-NvmSyncReleaseAssetPath -Version $ctx.CliVersion -Architecture $Architecture -DistRoot $ctx.DistRoot -New-Item -ItemType Directory -Force -Path (Split-Path -Parent $staged) | Out-Null -Copy-Item -LiteralPath $syncExePath -Destination $staged -Force -Write-Host "Staged release asset -> $staged" - -return $syncExePath diff --git a/build/steps/New-WingetManifest.ps1 b/build/steps/New-WingetManifest.ps1 deleted file mode 100644 index fc1d9632..00000000 --- a/build/steps/New-WingetManifest.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -<# -.SYNOPSIS -Creates an unsigned, local WinGet manifest set for a community release. - -.DESCRIPTION -Generates and hashes manifests only. This script never forks winget-pkgs, -opens a pull request, or submits a package. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$ReleaseTag, - - [Parameter(Mandatory = $true)] - [string]$Repository, - - [Parameter(Mandatory = $true)] - [string]$InstallerDirectory, - - [Parameter(Mandatory = $true)] - [string]$OutputDirectory -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$packageIdentifier = "AuthorSoftware.NVMWindows" -$publisher = "Author Software Inc." -$packageName = "NVM for Windows" -$productCode = "40078385-F676-4C61-9A9C-F9028599D6D3_is1" - -$tag = $ReleaseTag.Trim() -if ([string]::IsNullOrWhiteSpace($tag)) { - throw "ReleaseTag must not be empty." -} -$version = $tag -replace '^[vV]', '' -if ([string]::IsNullOrWhiteSpace($version)) { - throw "ReleaseTag does not contain a version: $ReleaseTag" -} -if ($Repository -notmatch '^[^/\s]+/[^/\s]+$') { - throw "Repository must be owner/name: $Repository" -} - -$installerRoot = [System.IO.Path]::GetFullPath($InstallerDirectory) -if (-not (Test-Path -LiteralPath $installerRoot -PathType Container)) { - throw "Installer directory does not exist: $installerRoot" -} - -$installers = @( - @{ FileArchitecture = "x64"; WingetArchitecture = "x64" } - @{ FileArchitecture = "arm64"; WingetArchitecture = "arm64" } -) - -$installerEntries = New-Object System.Collections.Generic.List[string] -foreach ($installer in $installers) { - $assetName = "nvm-{0}-{1}-setup.exe" -f $version, $installer.FileArchitecture - $assetPath = Join-Path $installerRoot $assetName - if (-not (Test-Path -LiteralPath $assetPath -PathType Leaf)) { - throw "Required release asset missing: $assetPath" - } - - $hash = (Get-FileHash -LiteralPath $assetPath -Algorithm SHA256).Hash.ToUpperInvariant() - $assetUrl = "https://github.com/{0}/releases/download/{1}/{2}" -f $Repository, $tag, $assetName - $installerEntries.Add(@" -- Architecture: $($installer.WingetArchitecture) - InstallerUrl: $assetUrl - InstallerSha256: $hash -"@.TrimEnd()) -} - -$manifestRoot = Join-Path $OutputDirectory ("manifests\a\AuthorSoftware\NVMWindows\{0}" -f $version) -New-Item -ItemType Directory -Force -Path $manifestRoot | Out-Null - -$versionManifest = @" -# yaml-language-server: `$schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json -PackageIdentifier: $packageIdentifier -PackageVersion: $version -DefaultLocale: en-US -ManifestType: version -ManifestVersion: 1.9.0 -"@ - -$localeManifest = @" -# yaml-language-server: `$schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json -PackageIdentifier: $packageIdentifier -PackageVersion: $version -PackageLocale: en-US -Publisher: $publisher -PublisherUrl: https://nvm-windows.com -PublisherSupportUrl: https://github.com/$Repository/issues -PackageName: $packageName -PackageUrl: https://github.com/$Repository -License: MIT -LicenseUrl: https://github.com/$Repository/blob/main/LICENSE -Copyright: Copyright (c) 2026 $publisher -CopyrightUrl: https://github.com/$Repository/blob/main/LICENSE -ShortDescription: Node.js version manager for Windows. -Moniker: nvm-windows -Tags: -- node -- nodejs -- nvm -- nvm-windows -- version-manager -- windows -ReleaseNotesUrl: https://github.com/$Repository/releases/tag/$tag -ManifestType: defaultLocale -ManifestVersion: 1.9.0 -"@ - -$installerManifest = @" -# yaml-language-server: `$schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json -PackageIdentifier: $packageIdentifier -PackageVersion: $version -InstallerLocale: en-US -InstallerType: inno -Scope: user -UpgradeBehavior: install -Commands: -- nvm -ProductCode: $productCode -AppsAndFeaturesEntries: -- Publisher: $publisher - ProductCode: $productCode -ElevationRequirement: elevatesSelf -InstallationMetadata: - DefaultInstallLocation: '%LocalAppData%\Author Software\nvm' -InstallerSwitches: - Silent: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART - SilentWithProgress: /SILENT /SUPPRESSMSGBOXES /NORESTART -Installers: -$($installerEntries -join "`n") -ManifestType: installer -ManifestVersion: 1.9.0 -"@ - -$encoding = [System.Text.UTF8Encoding]::new($false) -[System.IO.File]::WriteAllText((Join-Path $manifestRoot "$packageIdentifier.yaml"), $versionManifest, $encoding) -[System.IO.File]::WriteAllText((Join-Path $manifestRoot "$packageIdentifier.locale.en-US.yaml"), $localeManifest, $encoding) -[System.IO.File]::WriteAllText((Join-Path $manifestRoot "$packageIdentifier.installer.yaml"), $installerManifest, $encoding) - -Write-Host "Created WinGet dry-run manifests: $manifestRoot" -Get-ChildItem -LiteralPath $manifestRoot -File | ForEach-Object { - Write-Host " $($_.Name)" -} diff --git a/build/steps/Publish-GitHubRelease.ps1 b/build/steps/Publish-GitHubRelease.ps1 deleted file mode 100644 index 2b544712..00000000 --- a/build/steps/Publish-GitHubRelease.ps1 +++ /dev/null @@ -1,967 +0,0 @@ -# Gate or publish a GitHub Release for the community Inno Setup build. -# Version comes from cli/src/manifest.json. -# Uploads per architecture: -# nvm---setup.exe -# nvm---sync.exe (prebuilt sync for public -DownloadSync builds) -# -# Modes: -# Gate β€” fail fast before long build if tag burned / already complete -# Publish β€” draft β†’ upload assets β†’ publish -# -# Requires: gh CLI, GH_TOKEN with contents:write (workflow github.token). - -param( - [Parameter(Mandatory = $true)] - [ValidateSet("Gate", "Publish")] - [string]$Mode, - - [Parameter(Mandatory = $true)] - [ValidateSet("amd64", "arm64")] - [string[]]$Architectures, - - [switch]$OverrideExisting, - - [bool]$Enabled = $true, - - [string]$Version = "", - - [string]$AssetRoot = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") - -function ConvertTo-StringArray { - param( - [AllowNull()] - $Items - ) - if ($null -eq $Items) { - return , [string[]]@() - } - if ($Items -is [string]) { - return , [string[]]@($Items) - } - if ($Items -is [System.Collections.Generic.List[string]]) { - return , [string[]]$Items.ToArray() - } - if ($Items -is [System.Array]) { - return , [string[]]@($Items | ForEach-Object { [string]$_ }) - } - return , [string[]]@($Items | ForEach-Object { [string]$_ }) -} - -$architectures = @( - $Architectures | - ForEach-Object { $_.Trim().ToLowerInvariant() } | - Where-Object { $_ -in @("amd64", "arm64") } | - Select-Object -Unique -) -if ($architectures.Count -eq 0) { - throw "Architectures must include amd64 and/or arm64" -} - -$version = $Version.Trim() -if ([string]::IsNullOrWhiteSpace($version)) { - $ctx = Initialize-NvmBuildContext - $version = [string]$ctx.CliVersion -} - -function Get-NvmReleaseTag { - param([string]$Version) - $v = $Version.Trim() - if ([string]::IsNullOrWhiteSpace($v)) { - throw "CLI version is empty" - } - if ($v.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { - return $v - } - return "v$v" -} - -function Clear-NativeExitCode { - $global:LASTEXITCODE = 0 -} - -function Invoke-GhCapture { - param( - [Parameter(Mandatory = $true)] - [string[]]$GhArgs - ) - $prevEap = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $lines = @(& gh @GhArgs 2>&1) - $code = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $prevEap - Clear-NativeExitCode - } - return [pscustomobject]@{ - ExitCode = $code - Text = (($lines | ForEach-Object { "$_" }) -join "`n").Trim() - } -} - -function Test-ReleaseExists { - param([string]$Tag) - - $view = Invoke-GhCapture -GhArgs @("release", "view", $Tag, "--json", "databaseId") - if ($view.ExitCode -eq 0) { - return $true - } - - $api = Invoke-GhCapture -GhArgs @("api", "repos/{owner}/{repo}/releases/tags/$Tag", "--silent") - return ($api.ExitCode -eq 0) -} - -function Get-ReleaseView { - param( - [string]$Tag, - [string]$JsonFields = "databaseId,url,isDraft,tagName" - ) - $result = Invoke-GhCapture -GhArgs @("release", "view", $Tag, "--json", $JsonFields) - if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Text) -or ($result.Text -notmatch '^\s*\{')) { - return $null - } - return ($result.Text | ConvertFrom-Json) -} - -function Test-ReleaseIsDraft { - param([string]$Tag) - $view = Get-ReleaseView -Tag $Tag -JsonFields "isDraft" - if ($null -eq $view) { - return $false - } - return [bool]$view.isDraft -} - -function Get-ReleaseAssetNames { - param([string]$Tag) - $names = New-Object System.Collections.Generic.List[string] - $view = Get-ReleaseView -Tag $Tag -JsonFields "assets" - if ($null -eq $view -or $null -eq $view.assets) { - return ConvertTo-StringArray $names - } - foreach ($a in @($view.assets)) { - $n = [string]$a.name - if (-not [string]::IsNullOrWhiteSpace($n)) { - $names.Add($n) - } - } - return ConvertTo-StringArray $names -} - -function Test-ReleaseAssetNameMatchesArchitecture { - param( - [string]$Name, - [string]$Architecture, - [ValidateSet("setup", "sync", "any")] - [string]$Kind = "any" - ) - $arch = [regex]::Escape((Get-NvmReleaseArchToken -Architecture $Architecture)) - switch ($Kind) { - "setup" { return ($Name -match ("-{0}-setup\.exe$" -f $arch)) } - "sync" { return ($Name -match ("-{0}-sync\.exe$" -f $arch)) } - default { return ($Name -match ("-{0}-(setup|sync)\.exe$" -f $arch)) } - } -} - -function Get-ReleaseAssetsForArchitecture { - param( - [string]$Tag, - [string]$Architecture, - [ValidateSet("setup", "sync", "any")] - [string]$Kind = "any" - ) - $matched = New-Object System.Collections.Generic.List[string] - foreach ($name in (Get-ReleaseAssetNames -Tag $Tag)) { - if (Test-ReleaseAssetNameMatchesArchitecture -Name $name -Architecture $Architecture -Kind $Kind) { - $matched.Add($name) - } - } - return ConvertTo-StringArray $matched -} - -function Write-ReleaseEnv { - param( - [string]$Version, - [string]$Tag - ) - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { - Add-Content -LiteralPath $env:GITHUB_ENV -Value "CLI_VERSION=$Version" - Add-Content -LiteralPath $env:GITHUB_ENV -Value "RELEASE_TAG=$Tag" - } - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_OUTPUT)) { - Add-Content -LiteralPath $env:GITHUB_OUTPUT -Value "version=$Version" - Add-Content -LiteralPath $env:GITHUB_OUTPUT -Value "tag=$Tag" - } - Write-Host "CLI version -> $Version" - Write-Host "Release tag -> $Tag" - Write-Host ("Architectures -> {0}" -f ($architectures -join ", ")) -} - -function Get-ImmutableTagBurnHint { - param([string]$Tag) - return @" -Tag $Tag may be permanently reserved if it was ever published while immutable releases were enabled. -Deleting the release does NOT free the tag name in that case. - -Bump ``cli/src/manifest.json`` version and re-run. -Preferred flow: create --draft β†’ upload assets β†’ edit --draft=false -"@ -} - -function Get-NvmReleaseNotesFooter { - return @" - -- https://nvm-windows.com -- https://docs.nvm-windows.com - -> [!IMPORTANT] -> **`sync.exe` is only necessary when [building from source](https://docs.nvm-windows.com/install/source).** -"@ -} - -function Get-NvmGitHubRepository { - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_REPOSITORY)) { - return $env:GITHUB_REPOSITORY.Trim() - } - $remote = Invoke-GhCapture -GhArgs @("repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner") - if ($remote.ExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($remote.Text)) { - return $remote.Text.Trim() - } - return "nvm-windows/nvm" -} - -function Get-NvmReleaseHeadRef { - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_SHA)) { - return $env:GITHUB_SHA.Trim() - } - $git = Get-Command git -ErrorAction SilentlyContinue - if ($null -ne $git) { - $head = (& git rev-parse HEAD 2>$null) - if (-not [string]::IsNullOrWhiteSpace($head)) { - return $head.Trim() - } - } - return $null -} - -function Get-NvmPreviousPublishedReleaseTag { - param([string]$CurrentTag) - - $list = Invoke-GhCapture -GhArgs @("release", "list", "--limit", "100", "--json", "tagName,isDraft,publishedAt") - if ($list.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($list.Text)) { - return $null - } - $releases = @($list.Text | ConvertFrom-Json) | - Where-Object { -not $_.isDraft -and -not [string]::IsNullOrWhiteSpace([string]$_.tagName) } | - Sort-Object { [datetime]$_.publishedAt } -Descending - - foreach ($release in $releases) { - $tagName = [string]$release.tagName - if ($tagName -eq $CurrentTag) { - continue - } - return $tagName - } - return $null -} - -function Get-NvmGitHubCommitRefSha { - param( - [string]$Repository, - [string]$Ref - ) - if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($Ref)) { - return $null - } - $encoded = [uri]::EscapeDataString($Ref) - $result = Invoke-GhCapture -GhArgs @("api", "repos/$Repository/commits/$encoded", "-q", ".sha") - if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Text)) { - return $null - } - return $result.Text.Trim() -} - -function Get-NvmSubmoduleCommitAtRef { - param( - [string]$Repository, - [string]$Ref, - [string]$SubmodulePath - ) - $commitSha = Get-NvmGitHubCommitRefSha -Repository $Repository -Ref $Ref - if ([string]::IsNullOrWhiteSpace($commitSha)) { - return $null - } - $treeSha = (Invoke-GhCapture -GhArgs @("api", "repos/$Repository/commits/$commitSha", "-q", ".commit.tree.sha")).Text.Trim() - if ([string]::IsNullOrWhiteSpace($treeSha)) { - return $null - } - $treeJson = Invoke-GhCapture -GhArgs @("api", "repos/$Repository/git/trees/$treeSha") - if ($treeJson.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($treeJson.Text)) { - return $null - } - $tree = $treeJson.Text | ConvertFrom-Json - foreach ($entry in @($tree.tree)) { - if ([string]$entry.path -eq $SubmodulePath -and [string]$entry.type -eq "commit") { - return [string]$entry.sha - } - } - return $null -} - -function Format-NvmReleaseCommitLine { - param( - [string]$Repository, - $Commit - ) - $sha = [string]$Commit.sha - if ([string]::IsNullOrWhiteSpace($sha)) { - return $null - } - $short = if ($sha.Length -gt 7) { $sha.Substring(0, 7) } else { $sha } - $message = [string]$Commit.commit.message - $subject = ($message -split "`r?`n", 2)[0].Trim() - if ([string]::IsNullOrWhiteSpace($subject)) { - $subject = "commit" - } - $author = [string]$Commit.commit.author.name - if ([string]::IsNullOrWhiteSpace($author)) { - $author = [string]$Commit.author.login - } - if ([string]::IsNullOrWhiteSpace($author)) { - $author = "unknown" - } - $url = [string]$Commit.html_url - if ([string]::IsNullOrWhiteSpace($url)) { - $url = "https://github.com/$Repository/commit/$sha" - } - return "* [``$short``]($url) $subject ($author)" -} - -function Get-NvmCompareCommitLines { - param( - [string]$Repository, - [string]$BaseRef, - [string]$HeadRef - ) - if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($BaseRef) -or [string]::IsNullOrWhiteSpace($HeadRef)) { - return , [string[]]@() - } - if ($BaseRef -eq $HeadRef) { - return , [string[]]@() - } - $range = "$BaseRef...$HeadRef" - $result = Invoke-GhCapture -GhArgs @("api", "repos/$Repository/compare/$range") - if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Text)) { - Write-Host ("Release notes: unable to compare {0} ({1})" -f $Repository, $result.Text) - return , [string[]]@() - } - $compare = $result.Text | ConvertFrom-Json - $lines = New-Object System.Collections.Generic.List[string] - foreach ($commit in @($compare.commits)) { - $line = Format-NvmReleaseCommitLine -Repository $Repository -Commit $commit - if (-not [string]::IsNullOrWhiteSpace($line)) { - $lines.Add($line) - } - } - return ConvertTo-StringArray $lines -} - -function Get-NvmReleaseCompareRepoSections { - param( - [string]$Tag, - [string]$HeadRef - ) - $mainRepo = Get-NvmGitHubRepository - $owner = ($mainRepo -split "/", 2)[0] - if ([string]::IsNullOrWhiteSpace($owner)) { - $owner = "nvm-windows" - } - $previousTag = Get-NvmPreviousPublishedReleaseTag -CurrentTag $Tag - $previousHead = if ([string]::IsNullOrWhiteSpace($previousTag)) { $null } else { Get-NvmGitHubCommitRefSha -Repository $mainRepo -Ref $previousTag } - $currentHead = if (-not [string]::IsNullOrWhiteSpace($HeadRef)) { - $HeadRef - } - else { - Get-NvmGitHubCommitRefSha -Repository $mainRepo -Ref $Tag - } - - $repoSections = New-Object System.Collections.Generic.List[object] - if (-not [string]::IsNullOrWhiteSpace($previousHead) -and -not [string]::IsNullOrWhiteSpace($currentHead)) { - $repoSections.Add([pscustomobject]@{ - Label = $mainRepo - Repository = $mainRepo - Base = $previousHead - Head = $currentHead - }) - } - foreach ($sub in @("cli", "common", "shim", "sync")) { - $subRepo = "$owner/$sub" - $prevSub = if ($previousHead) { Get-NvmSubmoduleCommitAtRef -Repository $mainRepo -Ref $previousHead -SubmodulePath $sub } else { $null } - $headSub = if ($currentHead) { Get-NvmSubmoduleCommitAtRef -Repository $mainRepo -Ref $currentHead -SubmodulePath $sub } else { $null } - if ([string]::IsNullOrWhiteSpace($headSub) -or [string]::IsNullOrWhiteSpace($prevSub)) { - continue - } - $repoSections.Add([pscustomobject]@{ - Label = $subRepo - Repository = $subRepo - Base = $prevSub - Head = $headSub - }) - } - - return [pscustomobject]@{ - PreviousTag = $previousTag - Sections = @($repoSections.ToArray()) - } -} - -function Get-NvmCompareCommits { - param( - [string]$Repository, - [string]$BaseRef, - [string]$HeadRef - ) - if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($BaseRef) -or [string]::IsNullOrWhiteSpace($HeadRef)) { - return @() - } - if ($BaseRef -eq $HeadRef) { - return @() - } - $range = "$BaseRef...$HeadRef" - $result = Invoke-GhCapture -GhArgs @("api", "repos/$Repository/compare/$range") - if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Text)) { - Write-Host ("Release notes: unable to compare {0} ({1})" -f $Repository, $result.Text) - return @() - } - $compare = $result.Text | ConvertFrom-Json - return @($compare.commits) -} - -function Test-NvmAuthorHasPriorCommits { - param( - [string]$Repository, - [string]$Login, - [string]$BeforeSha - ) - if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($Login) -or [string]::IsNullOrWhiteSpace($BeforeSha)) { - return $true - } - $encodedLogin = [uri]::EscapeDataString($Login) - $path = "repos/$Repository/commits?author=$encodedLogin&sha=$BeforeSha&per_page=1" - $result = Invoke-GhCapture -GhArgs @("api", $path) - if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Text)) { - return $true - } - $commits = @($result.Text | ConvertFrom-Json) - return ($commits.Count -gt 0) -} - -function Get-NvmReleaseCommitSummarySection { - param( - [string]$Tag, - [string]$HeadRef - ) - $compare = Get-NvmReleaseCompareRepoSections -Tag $Tag -HeadRef $HeadRef - $previousTag = [string]$compare.PreviousTag - $sections = New-Object System.Collections.Generic.List[string] - - foreach ($section in @($compare.Sections)) { - if ($null -eq $section -or [string]::IsNullOrWhiteSpace([string]$section.Head) -or [string]::IsNullOrWhiteSpace([string]$section.Base)) { - continue - } - $lines = Get-NvmCompareCommitLines -Repository $section.Repository -BaseRef $section.Base -HeadRef $section.Head - if ($lines.Count -eq 0) { - continue - } - $sections.Add("### $($section.Label)") - foreach ($line in $lines) { - $sections.Add($line) - } - $sections.Add("") - } - - if ($sections.Count -eq 0) { - if ([string]::IsNullOrWhiteSpace($previousTag)) { - return "## Commits`n`n_No previous published release to compare._" - } - return "## Commits`n`n_No commits since $previousTag._" - } - while ($sections.Count -gt 0 -and [string]::IsNullOrWhiteSpace($sections[$sections.Count - 1])) { - $sections.RemoveAt($sections.Count - 1) - } - return ("## Commits`n`n" + ($sections -join "`n")) -} - -function Get-NvmReleaseContributorLogins { - param( - [string]$Tag, - [string]$HeadRef - ) - $compare = Get-NvmReleaseCompareRepoSections -Tag $Tag -HeadRef $HeadRef - $logins = New-Object System.Collections.Generic.List[string] - $seen = @{} - foreach ($section in @($compare.Sections)) { - if ($null -eq $section -or [string]::IsNullOrWhiteSpace([string]$section.Head) -or [string]::IsNullOrWhiteSpace([string]$section.Base)) { - continue - } - $commits = Get-NvmCompareCommits -Repository $section.Repository -BaseRef $section.Base -HeadRef $section.Head - foreach ($commit in $commits) { - $login = [string]$commit.author.login - if ([string]::IsNullOrWhiteSpace($login)) { - continue - } - if ($login -match '\[bot\]$' -or $login -eq "dependabot" -or $login -eq "renovate") { - continue - } - $key = $login.ToLowerInvariant() - if ($seen.ContainsKey($key)) { - continue - } - $seen[$key] = $true - $logins.Add($login) - } - } - return , [string[]]@($logins.ToArray()) -} - -function Get-NvmReleaseNewContributorsSection { - param( - [string]$Tag, - [string]$HeadRef - ) - $compare = Get-NvmReleaseCompareRepoSections -Tag $Tag -HeadRef $HeadRef - if ([string]::IsNullOrWhiteSpace([string]$compare.PreviousTag)) { - return "" - } - - # login -> first contribution url in this release window - $newByLogin = [ordered]@{} - foreach ($section in @($compare.Sections)) { - if ($null -eq $section -or [string]::IsNullOrWhiteSpace([string]$section.Head) -or [string]::IsNullOrWhiteSpace([string]$section.Base)) { - continue - } - $commits = Get-NvmCompareCommits -Repository $section.Repository -BaseRef $section.Base -HeadRef $section.Head - foreach ($commit in $commits) { - $login = [string]$commit.author.login - if ([string]::IsNullOrWhiteSpace($login)) { - continue - } - if ($login -match '\[bot\]$' -or $login -eq "dependabot" -or $login -eq "renovate") { - continue - } - if ($newByLogin.Contains($login)) { - continue - } - if (Test-NvmAuthorHasPriorCommits -Repository $section.Repository -Login $login -BeforeSha $section.Base) { - continue - } - $url = [string]$commit.html_url - if ([string]::IsNullOrWhiteSpace($url)) { - $sha = [string]$commit.sha - $url = "https://github.com/$($section.Repository)/commit/$sha" - } - $newByLogin[$login] = $url - } - } - - if ($newByLogin.Count -eq 0) { - return "" - } - - $lines = New-Object System.Collections.Generic.List[string] - $lines.Add("## New Contributors") - $lines.Add("") - foreach ($login in $newByLogin.Keys) { - $lines.Add("* @$login made their first contribution in $($newByLogin[$login])") - } - return ($lines -join "`n") -} - -function Get-NvmReleaseContributorsIconsSection { - param( - [string]$Tag, - [string]$HeadRef - ) - $logins = @(Get-NvmReleaseContributorLogins -Tag $Tag -HeadRef $HeadRef) - if ($logins.Count -eq 0) { - return "" - } - - # Avatar row similar to common OSS release notes (GitHub serves /{user}.png). - $icons = New-Object System.Collections.Generic.List[string] - foreach ($login in $logins) { - $href = "https://github.com/$login" - $src = "https://github.com/$login.png?size=64" - $icons.Add("`"@$login`"") - } - return ("## Contributors`n`n" + ($icons -join " ")) -} - -function Build-NvmReleaseNotesBody { - param( - [string]$Tag, - [string]$Version - ) - $noteArches = ($architectures -join ", ") - $headRef = Get-NvmReleaseHeadRef - $intro = @" -# NVM for Windows $Tag - -Authenticode-signed community build from ``cli/src/manifest.json`` version ``$Version``. -Architectures: $noteArches. -Program root: per-user LocalAppData (unsupported outside that root may log NVM4101). -Org Program Files / MSI / Intune: use NVM for Windows Certified Builds. - -Assets per arch: Inno Setup installer (``*-setup.exe``) and prebuilt ``sync.exe`` (``*-sync.exe``) for public ``-DownloadSync`` builds. -"@ - if ($Version -match '^\d+\.\d+\.\d+-') { - $intro += "`n`nPrerelease stamp applied at build time via the Community Release workflow (manifest base left unchanged in git)." - } - $commits = Get-NvmReleaseCommitSummarySection -Tag $Tag -HeadRef $headRef - $newContributors = Get-NvmReleaseNewContributorsSection -Tag $Tag -HeadRef $headRef - $contributorIcons = Get-NvmReleaseContributorsIconsSection -Tag $Tag -HeadRef $headRef - $footer = Get-NvmReleaseNotesFooter - $body = $intro.TrimEnd() + "`n`n" + $commits.TrimEnd() - if (-not [string]::IsNullOrWhiteSpace($newContributors)) { - $body += "`n`n" + $newContributors.TrimEnd() - } - if (-not [string]::IsNullOrWhiteSpace($contributorIcons)) { - $body += "`n`n" + $contributorIcons.TrimEnd() - } - return ($body + $footer) -} - -function Set-NvmReleaseNotes { - param( - [string]$Tag, - [string]$Version - ) - $notes = Build-NvmReleaseNotesBody -Tag $Tag -Version $Version - $notesFile = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-release-notes-{0}.md" -f ([guid]::NewGuid().ToString("n"))) - try { - [System.IO.File]::WriteAllText($notesFile, $notes, (New-Object System.Text.UTF8Encoding $false)) - $edit = Invoke-GhCapture -GhArgs @("release", "edit", $Tag, "--notes-file", $notesFile) - if ($edit.ExitCode -ne 0) { - throw ("gh release edit {0} --notes-file failed with exit code {1}: {2}" -f $Tag, $edit.ExitCode, $edit.Text) - } - } - finally { - if (Test-Path -LiteralPath $notesFile) { - Remove-Item -LiteralPath $notesFile -Force -ErrorAction SilentlyContinue - } - } -} - -function New-NvmDraftRelease { - param( - [string]$Tag, - [string]$Version - ) - - $notesFile = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-release-notes-{0}.md" -f ([guid]::NewGuid().ToString("n"))) - try { - $notesBody = Build-NvmReleaseNotesBody -Tag $Tag -Version $Version - [System.IO.File]::WriteAllText($notesFile, $notesBody, (New-Object System.Text.UTF8Encoding $false)) - $createArgs = @( - "release", "create", $Tag, - "--draft", - "--title", $Tag, - "--notes-file", $notesFile - ) - # Any stamped semver (x.y.z-*) is a GitHub prerelease (validation drops, not "latest"). - if ($Version -match '^\d+\.\d+\.\d+-') { - $createArgs += "--prerelease" - } - - Write-Host ("Creating draft release {0}..." -f $Tag) - $created = Invoke-GhCapture -GhArgs $createArgs - if ($created.ExitCode -eq 0) { - if (-not [string]::IsNullOrWhiteSpace($created.Text)) { - Write-Host $created.Text - } - return - } - - if (Test-ReleaseExists -Tag $Tag) { - Write-Host ("Release {0} already exists after create exit {1}; continuing." -f $Tag, $created.ExitCode) - return - } - - if ($created.Text -match '(?i)Resource not accessible by integration|HTTP 403') { - throw ("{0}`n`nGH_TOKEN cannot create releases (403). Use github.token with contents:write." -f $created.Text) - } - - if ($created.Text -match '(?i)immutable|tag_name was used') { - throw ("{0}`n`n{1}" -f $created.Text, (Get-ImmutableTagBurnHint -Tag $Tag)) - } - - if ($created.Text -match '(?i)creations being restricted|Repository rule') { - throw ("{0}`n`nRuleset blocked tag/ref creation. Allow github-actions to create tags, or bypass for this repo." -f $created.Text) - } - - throw ("gh release create --draft {0} failed with exit code {1}: {2}" -f $Tag, $created.ExitCode, $created.Text) - } - finally { - if (Test-Path -LiteralPath $notesFile) { - Remove-Item -LiteralPath $notesFile -Force -ErrorAction SilentlyContinue - } - } -} - -function Get-ReleaseUploadState { - param([string]$Tag) - - $view = Get-ReleaseView -Tag $Tag -JsonFields "isDraft,url,databaseId" - if ($null -eq $view) { - throw ("Release {0} not visible to GH_TOKEN. Check contents:write on github.token." -f $Tag) - } - return [pscustomobject]@{ - IsDraft = [bool]$view.isDraft - Url = [string]$view.url - DatabaseId = [string]$view.databaseId - } -} - -function Publish-NvmDraftRelease { - param([string]$Tag) - - Write-Host ("Publishing draft release {0}..." -f $Tag) - $pub = Invoke-GhCapture -GhArgs @("release", "edit", $Tag, "--draft=false") - if ($pub.ExitCode -ne 0) { - throw ("gh release edit {0} --draft=false failed with exit code {1}: {2}" -f $Tag, $pub.ExitCode, $pub.Text) - } -} - -function Remove-ReleaseAssetsForArchitecture { - param( - [string]$Tag, - [string]$Architecture - ) - $names = Get-ReleaseAssetsForArchitecture -Tag $Tag -Architecture $Architecture -Kind "any" - if ($names.Count -eq 0) { - Write-Host ("No existing {0} setup/sync assets on {1} to remove." -f $Architecture, $Tag) - return - } - foreach ($name in $names) { - Write-Host ("Removing release asset {0}..." -f $name) - $del = Invoke-GhCapture -GhArgs @("release", "delete-asset", $Tag, $name, "--yes") - if ($del.ExitCode -ne 0) { - throw "gh release delete-asset $Tag $name failed with exit code $($del.ExitCode): $($del.Text)" - } - } -} - -function Get-NvmReleaseUploadPaths { - param( - [string]$Architecture, - [string]$SearchRoot, - [ValidateSet("setup", "sync")] - [string]$Kind - ) - - $paths = New-Object System.Collections.Generic.List[string] - if (-not (Test-Path -LiteralPath $SearchRoot -PathType Container)) { - return ConvertTo-StringArray $paths - } - - Get-ChildItem -LiteralPath $SearchRoot -File -Recurse -ErrorAction SilentlyContinue | - Where-Object { - $_.Extension -ieq ".exe" -and - (Test-ReleaseAssetNameMatchesArchitecture -Name $_.Name -Architecture $Architecture -Kind $Kind) - } | - ForEach-Object { $paths.Add($_.FullName) } - - $byName = @{} - foreach ($p in $paths) { - $name = [System.IO.Path]::GetFileName($p) - if (-not $byName.ContainsKey($name)) { - $byName[$name] = $p - } - else { - Write-Host ("Release upload: skip duplicate name {0} ({1})" -f $name, $p) - } - } - $unique = New-Object System.Collections.Generic.List[string] - foreach ($name in ($byName.Keys | Sort-Object)) { - $unique.Add([string]$byName[$name]) - } - return ConvertTo-StringArray $unique -} - -$tag = Get-NvmReleaseTag -Version $version -Write-ReleaseEnv -Version $version -Tag $tag - -if (-not $Enabled) { - Write-Host "GitHub Release publish disabled (publish_release unchecked)." -ForegroundColor Yellow - Clear-NativeExitCode - return -} - -if ([string]::IsNullOrWhiteSpace($env:GH_TOKEN) -and [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { - throw "GH_TOKEN or GITHUB_TOKEN required for release Gate/Publish" -} - -$repoRoot = Get-NvmRepoRoot -$defaultSearchRoot = if (-not [string]::IsNullOrWhiteSpace($AssetRoot)) { - [System.IO.Path]::GetFullPath($AssetRoot) -} -else { - $repoRoot -} - -switch ($Mode) { - "Gate" { - if (-not (Test-ReleaseExists -Tag $tag)) { - Write-Host ("Probing draft create for {0}..." -f $tag) - New-NvmDraftRelease -Tag $tag -Version $version - Write-Host ("Draft {0} ready β€” build may proceed." -f $tag) - Clear-NativeExitCode - return - } - - $isDraft = Test-ReleaseIsDraft -Tag $tag - $missing = New-Object System.Collections.Generic.List[string] - $conflicts = New-Object System.Collections.Generic.List[string] - - foreach ($arch in $architectures) { - $setupAssets = Get-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch -Kind "setup" - $syncAssets = Get-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch -Kind "sync" - $complete = ($setupAssets.Count -gt 0 -and $syncAssets.Count -gt 0) - - if (-not $complete) { - $missing.Add($arch) | Out-Null - Write-Host ("Release {0} ({1}) incomplete for {2}: setup={3} sync={4}." -f $tag, $(if ($isDraft) { "draft" } else { "published" }), $arch, $setupAssets.Count, $syncAssets.Count) - continue - } - if ($OverrideExisting) { - Write-Host ("Release {0} has complete {1} assets; override_existing_release=true β€” will replace after build:" -f $tag, $arch) - @($setupAssets) + @($syncAssets) | ForEach-Object { Write-Host (" - {0}" -f $_) } - continue - } - foreach ($name in (@($setupAssets) + @($syncAssets))) { - $conflicts.Add("${arch}: $name") | Out-Null - } - } - - if ($conflicts.Count -gt 0) { - throw @" -Release $tag already has setup+sync assets for selected architecture(s): -$($conflicts | ForEach-Object { " - $_" } | Out-String) -Re-run with override_existing_release=true to replace them (immutable must be OFF), or bump cli/src/manifest.json version. -"@ - } - - if ($missing.Count -eq 0 -and -not $OverrideExisting) { - throw @" -Release $tag already has setup.exe + sync.exe for all selected architecture(s): $($architectures -join ', '). -Nothing to build/publish. Bump cli/src/manifest.json version, or re-run with override_existing_release=true (immutable must be OFF). -"@ - } - - if (-not $isDraft -and $missing.Count -gt 0) { - Write-Host ("Published release {0} missing {1} β€” will upload after build (requires immutable OFF)." -f $tag, ($missing -join ", ")) -ForegroundColor Yellow - } - } - - "Publish" { - if (-not (Test-ReleaseExists -Tag $tag)) { - New-NvmDraftRelease -Tag $tag -Version $version - } - - $state = Get-ReleaseUploadState -Tag $tag - if ($state.IsDraft) { - Write-Host ("Target release {0} is draft β€” upload setup+sync then publish." -f $tag) - } - else { - Write-Host ("Target release {0} is already published β€” uploading with --clobber (immutable must be OFF)." -f $tag) -ForegroundColor Yellow - } - - foreach ($arch in $architectures) { - $setupAssets = Get-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch -Kind "setup" - $syncAssets = Get-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch -Kind "sync" - $hasAny = ($setupAssets.Count -gt 0 -or $syncAssets.Count -gt 0) - $complete = ($setupAssets.Count -gt 0 -and $syncAssets.Count -gt 0) - - if ($complete -and -not $OverrideExisting) { - throw ("Release {0} already has complete {1} setup+sync assets and override_existing_release is false." -f $tag, $arch) - } - if ($hasAny -and $OverrideExisting) { - Remove-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch - } - elseif ($hasAny -and -not $complete) { - Write-Host ("Release {0} incomplete for {1} (setup={2} sync={3}) β€” uploading missing assets without purge." -f $tag, $arch, $setupAssets.Count, $syncAssets.Count) -ForegroundColor Yellow - } - } - - $upload = New-Object System.Collections.Generic.List[string] - $uploadSeenPath = @{} - $uploadSeenName = @{} - foreach ($arch in $architectures) { - foreach ($kind in @("setup", "sync")) { - $existingKind = Get-ReleaseAssetsForArchitecture -Tag $tag -Architecture $arch -Kind $kind - if ($existingKind.Count -gt 0 -and -not $OverrideExisting) { - Write-Host ("Keeping existing {0} asset(s) for {1}; skip upload of that kind." -f $kind, $arch) - continue - } - $found = Get-NvmReleaseUploadPaths -Architecture $arch -SearchRoot $defaultSearchRoot -Kind $kind - if ($found.Count -eq 0) { - throw ("No {0}-{1}.exe found under {2}" -f $arch, $kind, $defaultSearchRoot) - } - if ($found.Count -gt 1) { - Write-Host ("WARNING: multiple {0}-{1}.exe matches; uploading first unique names only." -f $arch, $kind) -ForegroundColor Yellow - } - foreach ($p in $found) { - $base = [System.IO.Path]::GetFileName($p) - if ($uploadSeenPath.ContainsKey($p) -or $uploadSeenName.ContainsKey($base)) { - continue - } - $uploadSeenPath[$p] = $true - $uploadSeenName[$base] = $true - $upload.Add($p) - } - } - } - - if ($upload.Count -eq 0) { - throw ("Nothing to upload for {0} (all setup+sync assets already present)." -f $tag) - } - - Write-Host ("Uploading {0} asset(s) to {1}..." -f $upload.Count, $tag) - $upload | ForEach-Object { Write-Host (" + {0}" -f $_) } - - $up = Invoke-GhCapture -GhArgs (@("release", "upload", $tag) + @($upload.ToArray()) + @("--clobber")) - if ($up.ExitCode -ne 0) { - throw "gh release upload $tag failed with exit code $($up.ExitCode): $($up.Text)" - } - - Set-NvmReleaseNotes -Tag $tag -Version $version - - if ($state.IsDraft) { - Publish-NvmDraftRelease -Tag $tag - } - else { - Write-Host ("Release {0} was already published β€” skipped draftβ†’publish." -f $tag) - } - - $view = Get-ReleaseView -Tag $tag -JsonFields "url,databaseId,isDraft" - if ($null -eq $view -or [string]::IsNullOrWhiteSpace([string]$view.databaseId)) { - throw "Unable to read GitHub release databaseId for $tag after publish" - } - $url = [string]$view.url - $releaseId = [string]$view.databaseId - Write-Host ("Release ready -> {0} (id {1}, draft={2})" -f $url, $releaseId, $view.isDraft) - if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_STEP_SUMMARY)) { - $archList = $architectures -join ", " - Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value ("## GitHub Release`n`n[{0}]({1})`n`nArchitectures: {2}`n`nRelease ID: ``{3}```n`nAssets: ``*-setup.exe`` + ``*-sync.exe``" -f $tag, $url, $archList, $releaseId) - } - } -} - -Clear-NativeExitCode diff --git a/build/steps/Sign-CommunityArtifacts.ps1 b/build/steps/Sign-CommunityArtifacts.ps1 deleted file mode 100644 index 0694c26c..00000000 --- a/build/steps/Sign-CommunityArtifacts.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -param( - [ValidateSet("amd64", "arm64")] - [string]$Architecture = "amd64", - [string]$BinRoot = "", - [switch]$IncludeInstaller, - [switch]$InstallerOnly -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -. (Join-Path $PSScriptRoot "..\common.ps1") -$ctx = Initialize-NvmBuildContext -BinRoot $BinRoot - -$signScript = Join-Path $PSScriptRoot "..\scripts\Sign-Executables.ps1" -if (-not (Test-Path -LiteralPath $signScript -PathType Leaf)) { - throw "Sign-Executables.ps1 not found: $signScript" -} - -$ensureScript = Join-Path $PSScriptRoot "..\scripts\Ensure-ArtifactSigning.ps1" -if (Test-Path -LiteralPath $ensureScript -PathType Leaf) { - & $ensureScript *>&1 | ForEach-Object { Write-Host "$_" } -} - -$signable = New-Object System.Collections.Generic.List[string] -if (-not $InstallerOnly) { - foreach ($path in (Get-NvmExpectedExePaths -BinRoot $ctx.BinRoot -Component All)) { - Assert-NvmFile -Path $path -Label ([System.IO.Path]::GetFileName($path)) | Out-Null - $signable.Add($path) - } - foreach ($path in (Get-NvmExpectedEventProviderPaths -BinRoot $ctx.BinRoot)) { - if ([System.IO.Path]::GetExtension($path) -ine ".dll") { - continue - } - Assert-NvmFile -Path $path -Label ([System.IO.Path]::GetFileName($path)) | Out-Null - $signable.Add($path) - } -} - -if ($IncludeInstaller -or $InstallerOnly) { - $setup = Get-NvmInstallerSetupPath -Version $ctx.CliVersion -Architecture $Architecture -DistRoot $ctx.DistRoot - $syncAsset = Get-NvmSyncReleaseAssetPath -Version $ctx.CliVersion -Architecture $Architecture -DistRoot $ctx.DistRoot - Assert-NvmFile -Path $setup -Label ([System.IO.Path]::GetFileName($setup)) | Out-Null - Assert-NvmFile -Path $syncAsset -Label ([System.IO.Path]::GetFileName($syncAsset)) | Out-Null - $signable.Add($setup) - $signable.Add($syncAsset) -} - -if ($signable.Count -eq 0) { - throw "No community artifacts selected for signing" -} - -$correlation = [Environment]::GetEnvironmentVariable("ARTIFACT_SIGNING_CORRELATION_ID") -if ([string]::IsNullOrWhiteSpace($correlation) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_RUN_ID)) { - $correlation = "gha-community-{0}-{1}" -f $env:GITHUB_RUN_ID, $env:GITHUB_RUN_ATTEMPT -} - -$signArgs = @{ Path = $signable.ToArray() } -$dlibFromEnv = [Environment]::GetEnvironmentVariable("ARTIFACT_SIGNING_DLIB") -if (-not [string]::IsNullOrWhiteSpace($dlibFromEnv) -and (Test-Path -LiteralPath $dlibFromEnv -PathType Leaf)) { - $signArgs["DlibPath"] = $dlibFromEnv -} -if (-not [string]::IsNullOrWhiteSpace($correlation)) { - $signArgs["CorrelationId"] = $correlation -} - -Write-Host "Signing community artifacts ($($signable.Count) file(s))..." -& $signScript @signArgs diff --git a/build/steps/Update-ReadmeReleaseBadges.ps1 b/build/steps/Update-ReadmeReleaseBadges.ps1 deleted file mode 100644 index 41488322..00000000 --- a/build/steps/Update-ReadmeReleaseBadges.ps1 +++ /dev/null @@ -1,301 +0,0 @@ -# Refresh README download badges from GitHub Releases. -# - Prerelease badge -> newest published pre-release (tag link); HTML-commented when -# it is not newer than the latest stable release -# - Stable badge -> newest published non-prerelease (tag link) -# -# Requires: gh CLI, GH_TOKEN with contents:read (list releases). - -param( - [string]$Repository = $env:GITHUB_REPOSITORY, - [string]$ReadmePath = "", - [switch]$WhatIf -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -if ([string]::IsNullOrWhiteSpace($Repository)) { - throw "Repository is required (GITHUB_REPOSITORY or -Repository)." -} - -if ([string]::IsNullOrWhiteSpace($ReadmePath)) { - $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - $ReadmePath = Join-Path $repoRoot "README.md" -} - -if (-not (Test-Path -LiteralPath $ReadmePath -PathType Leaf)) { - throw "README not found: $ReadmePath" -} - -$markerStart = "" -$markerEnd = "" - -function Invoke-GhJson { - param( - [Parameter(Mandatory = $true)] - [string[]]$GhArgs - ) - $prevEap = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $text = (& gh @GhArgs --repo $Repository 2>&1 | Out-String).Trim() - $code = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $prevEap - } - if ($code -ne 0) { - throw "gh $($GhArgs -join ' ') failed ($code): $text" - } - if ([string]::IsNullOrWhiteSpace($text)) { - return $null - } - return $text | ConvertFrom-Json -} - -function Encode-ShieldsPathPart { - param([string]$Text) - # shields.io path badges: '-' separates fields, '--' is a literal hyphen. - $escaped = $Text.Replace('_', '__').Replace('-', '--') - return [uri]::EscapeDataString($escaped).Replace('+', '%20') -} - -function Format-PrereleaseBadgeLabel { - param([string]$Tag) - return "Prerelease" -} - -function Format-StableBadgeLabel { - param([string]$Tag) - return "Install Stable Version" -} - -function Get-ReleasePageUrl { - param([string]$Tag) - return "https://github.com/$Repository/releases/tag/$Tag" -} - -function Build-PrereleaseBadgeMarkdown { - param( - [string]$Tag, - [string]$Label - ) - # Social-style "Latest Release" replacement; version in message for clarity. - $version = $Tag.Trim() - if ($version.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { - $version = $version.Substring(1) - } - $shields = "https://img.shields.io/badge/$(Encode-ShieldsPathPart $Label)-$(Encode-ShieldsPathPart "v$version")-1?style=social" - $url = Get-ReleasePageUrl -Tag $Tag - return "[![$Label]($shields)]($url)" -} - -function Build-StableBadgeMarkdown { - param( - [string]$Tag, - [string]$Label - ) - # Blue CTA (was "Install Now"). - $shields = "https://img.shields.io/badge/-$(Encode-ShieldsPathPart $Label)-%2322A6F2" - $url = Get-ReleasePageUrl -Tag $Tag - return "[![$Label]($shields)]($url)" -} - -function Get-StaticSocialBadgesMarkdown { - $downloads = "![Downloads](https://img.shields.io/github/downloads/$Repository/total?label=Downloads&style=social)" - $tweet = '[![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fhashtags%3Dnodejs%26original_referer%3Dhttp%253A%252F%252F127.0.0.1%253A91%252F%26text%3DNVM%2520for%2520Windows%2520v2%2520is%2520available%21%26tw_p%3Dtweetbutton%26url%3Dhttps%253A%252F%252Fnvm-windows.com)](https://twitter.com/intent/tweet?hashtags=nodejs&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=NVM%20for%20Windows%20v2%20is%20available.&tw_p=tweetbutton&url=https%3A%2F%2Fnvm-windows.com)' - return "$downloads $tweet" -} - -function Get-SemVerParts { - param([string]$Tag) - $version = $Tag.Trim() - if ($version.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { - $version = $version.Substring(1) - } - if ($version -notmatch '^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$') { - throw "Unsupported release tag for version compare: $Tag" - } - $prerelease = $null - if ($matches.Count -ge 5 -and -not [string]::IsNullOrWhiteSpace($matches[4])) { - $prerelease = $matches[4] - } - return [pscustomobject]@{ - Major = [int]$matches[1] - Minor = [int]$matches[2] - Patch = [int]$matches[3] - Prerelease = $prerelease - } -} - -function Compare-PrereleaseIdentifiers { - param( - [string]$Left, - [string]$Right - ) - $leftParts = if ([string]::IsNullOrWhiteSpace($Left)) { @() } else { $Left.Split('.') } - $rightParts = if ([string]::IsNullOrWhiteSpace($Right)) { @() } else { $Right.Split('.') } - $max = [Math]::Max($leftParts.Count, $rightParts.Count) - for ($i = 0; $i -lt $max; $i++) { - $leftPart = if ($i -lt $leftParts.Count) { $leftParts[$i] } else { $null } - $rightPart = if ($i -lt $rightParts.Count) { $rightParts[$i] } else { $null } - if ($null -eq $leftPart) { return -1 } - if ($null -eq $rightPart) { return 1 } - - $leftNumeric = 0 - $rightNumeric = 0 - $leftIsNumeric = [int]::TryParse($leftPart, [ref]$leftNumeric) - $rightIsNumeric = [int]::TryParse($rightPart, [ref]$rightNumeric) - if ($leftIsNumeric -and $rightIsNumeric) { - if ($leftNumeric -lt $rightNumeric) { return -1 } - if ($leftNumeric -gt $rightNumeric) { return 1 } - continue - } - if ($leftIsNumeric -and -not $rightIsNumeric) { return -1 } - if (-not $leftIsNumeric -and $rightIsNumeric) { return 1 } - $cmp = [string]::Compare($leftPart, $rightPart, [System.StringComparison]::OrdinalIgnoreCase) - if ($cmp -ne 0) { return $cmp } - } - return 0 -} - -function Compare-ReleaseTags { - param( - [string]$LeftTag, - [string]$RightTag - ) - $leftVersion = Get-SemVerParts -Tag $LeftTag - $rightVersion = Get-SemVerParts -Tag $RightTag - if ($leftVersion.Major -ne $rightVersion.Major) { return [Math]::Sign($leftVersion.Major - $rightVersion.Major) } - if ($leftVersion.Minor -ne $rightVersion.Minor) { return [Math]::Sign($leftVersion.Minor - $rightVersion.Minor) } - if ($leftVersion.Patch -ne $rightVersion.Patch) { return [Math]::Sign($leftVersion.Patch - $rightVersion.Patch) } - $leftPre = [string]$leftVersion.Prerelease - $rightPre = [string]$rightVersion.Prerelease - if ([string]::IsNullOrWhiteSpace($leftPre) -and [string]::IsNullOrWhiteSpace($rightPre)) { return 0 } - if ([string]::IsNullOrWhiteSpace($leftPre)) { return 1 } - if ([string]::IsNullOrWhiteSpace($rightPre)) { return -1 } - return Compare-PrereleaseIdentifiers -Left $leftPre -Right $rightPre -} - -function Test-PrereleaseIsNewerThanStable { - param( - [string]$PrereleaseTag, - [string]$StableTag - ) - return (Compare-ReleaseTags -LeftTag $PrereleaseTag -RightTag $StableTag) -gt 0 -} - -function Get-LatestPublishedPrerelease { - $releases = Invoke-GhJson -GhArgs @( - "release", "list", - "--limit", "100", - "--json", "tagName,isPrerelease,isDraft,publishedAt" - ) - if ($null -eq $releases) { - return $null - } - $list = @($releases) | Where-Object { - -not $_.isDraft -and $_.isPrerelease -and -not [string]::IsNullOrWhiteSpace([string]$_.tagName) - } - if ($list.Count -eq 0) { - return $null - } - return $list[0] -} - -function Get-LatestPublishedStableRelease { - $releases = Invoke-GhJson -GhArgs @( - "release", "list", - "--limit", "100", - "--exclude-pre-releases", - "--json", "tagName,isDraft,publishedAt" - ) - if ($null -eq $releases) { - return $null - } - $list = @($releases) | Where-Object { - -not $_.isDraft -and -not [string]::IsNullOrWhiteSpace([string]$_.tagName) - } - if ($list.Count -eq 0) { - return $null - } - return $list[0] -} - -$prerelease = Get-LatestPublishedPrerelease -$stable = Get-LatestPublishedStableRelease - -if ($null -eq $prerelease -and $null -eq $stable) { - Write-Host "No published releases found; README unchanged." - exit 0 -} - -$badgeParts = @() - -# Order matches classic README: Install CTA first, then prerelease indicator. -if ($null -ne $stable) { - $stableTag = [string]$stable.tagName - $stableLabel = Format-StableBadgeLabel -Tag $stableTag - $badgeParts += (Build-StableBadgeMarkdown -Tag $stableTag -Label $stableLabel) - Write-Host "Stable badge -> $stableLabel ($stableTag)" -} -else { - Write-Warning "No published stable release found; stable badge will be omitted." -} - -if ($null -ne $prerelease) { - $preTag = [string]$prerelease.tagName - $preLabel = Format-PrereleaseBadgeLabel -Tag $preTag - $preBadge = Build-PrereleaseBadgeMarkdown -Tag $preTag -Label $preLabel - $showPre = $true - if ($null -ne $stable) { - $stableTag = [string]$stable.tagName - $showPre = Test-PrereleaseIsNewerThanStable -PrereleaseTag $preTag -StableTag $stableTag - } - if ($showPre) { - $badgeParts += $preBadge - Write-Host "Prerelease badge -> $preLabel ($preTag) [visible]" - } - else { - $badgeParts += "" - Write-Host "Prerelease badge -> $preLabel ($preTag) [commented: not newer than stable]" - } -} -else { - Write-Warning "No published prerelease found; prerelease badge will be omitted." -} - -if ($badgeParts.Count -eq 0) { - Write-Host "Nothing to update." - exit 0 -} - -# Keep Downloads / tweet on the same visual row as release badges. -$badgeParts += (Get-StaticSocialBadgesMarkdown) - -$newInner = ($badgeParts -join " ") -# Newlines only around HTML markers β€” badges stay one line so they render in a row. -$newBlock = "$markerStart`n$newInner`n$markerEnd" - -$readme = [System.IO.File]::ReadAllText($ReadmePath) -if ($readme -notmatch [regex]::Escape($markerStart) -or $readme -notmatch [regex]::Escape($markerEnd)) { - throw "README is missing release badge markers ($markerStart ... $markerEnd)." -} - -$pattern = "(?s)$([regex]::Escape($markerStart)).*?$([regex]::Escape($markerEnd))" -$updated = [regex]::Replace($readme, $pattern, $newBlock, 1) - -if ($updated -eq $readme) { - Write-Host "README badges already up to date." - exit 0 -} - -if ($WhatIf) { - Write-Host "WhatIf: would update README badges:" - Write-Host $newInner - exit 0 -} - -$utf8NoBom = New-Object System.Text.UTF8Encoding $false -[System.IO.File]::WriteAllText($ReadmePath, $updated, $utf8NoBom) -Write-Host "Updated $ReadmePath" diff --git a/build/steps/Update-ThanksContributors.ps1 b/build/steps/Update-ThanksContributors.ps1 deleted file mode 100644 index c7f2597d..00000000 --- a/build/steps/Update-ThanksContributors.ps1 +++ /dev/null @@ -1,220 +0,0 @@ -# Refresh THANKS.md contributor grid from git history on selected refs. -# GitHub's /contributors + contrib.rocks only see the default branch (v2 rewrite), -# so this walks commit authors on historical refs too (v1 master + current main). -# -# Requires: gh CLI, GH_TOKEN with contents:read. - -param( - [string]$Repository = $env:GITHUB_REPOSITORY, - [string]$ThanksPath = "", - [string[]]$Refs = @("master", "main"), - [int]$AvatarSize = 64, - [int]$Columns = 8, - [switch]$WhatIf -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -if ([string]::IsNullOrWhiteSpace($Repository)) { - throw "Repository is required (GITHUB_REPOSITORY or -Repository)." -} - -if ([string]::IsNullOrWhiteSpace($ThanksPath)) { - $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - $ThanksPath = Join-Path $repoRoot "THANKS.md" -} - -if (-not (Test-Path -LiteralPath $ThanksPath -PathType Leaf)) { - throw "THANKS.md not found: $ThanksPath" -} - -$markerStart = "" -$markerEnd = "" - -$excludedLogins = @( - "github-actions[bot]", - "dependabot[bot]", - "dependabot-preview[bot]", - "renovate[bot]", - "imgbot[bot]", - "weblate", - "gitter-badger", - "codetriage-readme-bot" -) - -function Test-ExcludedLogin { - param([string]$Login) - if ([string]::IsNullOrWhiteSpace($Login)) { - return $true - } - if ($Login -match '\[bot\]$') { - return $true - } - foreach ($name in $excludedLogins) { - if ([string]::Equals($Login, $name, [System.StringComparison]::OrdinalIgnoreCase)) { - return $true - } - } - return $false -} - -function Invoke-GhApiJson { - param( - [Parameter(Mandatory = $true)] - [string]$PathAndQuery - ) - $prevEap = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $text = (& gh api --paginate $PathAndQuery 2>&1 | Out-String).Trim() - $code = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $prevEap - } - if ($code -ne 0) { - throw "gh api $PathAndQuery failed ($code): $text" - } - if ([string]::IsNullOrWhiteSpace($text)) { - return @() - } - $parsed = $text | ConvertFrom-Json - return @($parsed) -} - -function Get-LoginFromCommit { - param($Commit) - if ($null -ne $Commit.author -and -not [string]::IsNullOrWhiteSpace([string]$Commit.author.login)) { - return [string]$Commit.author.login - } - $email = "" - if ($null -ne $Commit.commit -and $null -ne $Commit.commit.author) { - $email = [string]$Commit.commit.author.email - } - if ($email -match '^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$') { - return $matches[1] - } - return "" -} - -function Get-ContributorsFromRef { - param([string]$RefName) - $encodedRef = [uri]::EscapeDataString($RefName) - $path = "repos/$Repository/commits?sha=$encodedRef&per_page=100" - Write-Host "Fetching commits -> $RefName" - return (Invoke-GhApiJson -PathAndQuery $path) -} - -$seenShas = New-Object "System.Collections.Generic.HashSet[string]" -$counts = @{} -$htmlUrls = @{} -$avatarUrls = @{} - -foreach ($refName in $Refs) { - $commits = Get-ContributorsFromRef -RefName $refName - Write-Host " $($commits.Count) commit records" - foreach ($commit in $commits) { - $sha = [string]$commit.sha - if ([string]::IsNullOrWhiteSpace($sha)) { - continue - } - if (-not $seenShas.Add($sha)) { - continue - } - $login = Get-LoginFromCommit -Commit $commit - if (Test-ExcludedLogin -Login $login) { - continue - } - if ($counts.ContainsKey($login)) { - $counts[$login] = [int]$counts[$login] + 1 - } - else { - $counts[$login] = 1 - } - if ($null -ne $commit.author) { - if (-not [string]::IsNullOrWhiteSpace([string]$commit.author.html_url)) { - $htmlUrls[$login] = [string]$commit.author.html_url - } - if (-not [string]::IsNullOrWhiteSpace([string]$commit.author.avatar_url)) { - $avatarUrls[$login] = [string]$commit.author.avatar_url - } - } - } -} - -if ($counts.Count -eq 0) { - throw "No contributors found for refs: $($Refs -join ', ')" -} - -$ranked = $counts.Keys | Sort-Object { - - [int]$counts[$_] -}, { $_.ToLowerInvariant() } - -Write-Host "Contributors -> $($ranked.Count) (bots excluded)" - -$cells = New-Object System.Collections.Generic.List[string] -foreach ($login in $ranked) { - $url = if ($htmlUrls.ContainsKey($login)) { $htmlUrls[$login] } else { "https://github.com/$login" } - $avatar = if ($avatarUrls.ContainsKey($login)) { - $avatarUrls[$login] - } - else { - "https://github.com/$login.png?size=$AvatarSize" - } - if ($avatar -notmatch '[\?&]s=') { - $sep = if ($avatar.Contains("?")) { "&" } else { "?" } - $avatar = "$avatar$sep" + "s=$AvatarSize" - } - $count = [int]$counts[$login] - $escapedLogin = [System.Net.WebUtility]::HtmlEncode($login) - $cells.Add( - ('{4}
{4}

{5}' -f ` - ([int][Math]::Floor(100 / $Columns)), $url, $avatar, $AvatarSize, $escapedLogin, $count) - ) -} - -$rowHtml = New-Object System.Collections.Generic.List[string] -$rowHtml.Add('') -for ($i = 0; $i -lt $cells.Count; $i += $Columns) { - $rowHtml.Add("") - $end = [Math]::Min($i + $Columns, $cells.Count) - 1 - for ($j = $i; $j -le $end; $j++) { - $rowHtml.Add($cells[$j]) - } - $rowHtml.Add("") -} -$rowHtml.Add("
") - -$generated = @( - "_Commit authors on ``master`` (v1) and ``main`` (v2), sorted by commit count. Bots omitted. Updated on each community release._" - "" - ($rowHtml -join "`n") -) -join "`n" - -$newBlock = "$markerStart`n$generated`n$markerEnd" - -$doc = [System.IO.File]::ReadAllText($ThanksPath) -if ($doc -notmatch [regex]::Escape($markerStart) -or $doc -notmatch [regex]::Escape($markerEnd)) { - throw "THANKS.md is missing contributor markers ($markerStart ... $markerEnd)." -} - -$pattern = "(?s)$([regex]::Escape($markerStart)).*?$([regex]::Escape($markerEnd))" -$rx = New-Object System.Text.RegularExpressions.Regex($pattern) -$safeBlock = $newBlock.Replace('$', '$$') -$updated = $rx.Replace($doc, $safeBlock, 1) - -if ($updated -eq $doc) { - Write-Host "THANKS.md contributors already up to date." - exit 0 -} - -if ($WhatIf) { - Write-Host "WhatIf: would write $($ranked.Count) contributors." - Write-Host ($ranked | Select-Object -First 10 | ForEach-Object { "$_ $($counts[$_])" } | Out-String) - exit 0 -} - -$utf8NoBom = New-Object System.Text.UTF8Encoding $false -[System.IO.File]::WriteAllText($ThanksPath, $updated, $utf8NoBom) -Write-Host "Updated $ThanksPath" diff --git a/build/steps/Update-VirusTotalReleaseNotes.ps1 b/build/steps/Update-VirusTotalReleaseNotes.ps1 deleted file mode 100644 index 0ada47cf..00000000 --- a/build/steps/Update-VirusTotalReleaseNotes.ps1 +++ /dev/null @@ -1,100 +0,0 @@ -# Rebuild the ## VirusTotal section on a GitHub Release from current asset files. -# Call after VirusTotal upload so hashes are indexed. Safe for override_existing_release -# republishes β€” always replaces any prior VirusTotal block with links for the files on disk. -# -# Requires: gh CLI, GH_TOKEN with contents:write - -param( - [Parameter(Mandatory = $true)] - [string]$Tag, - - [Parameter(Mandatory = $true)] - [string]$AssetRoot -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -if ([string]::IsNullOrWhiteSpace($Tag)) { - throw "Tag is required" -} -if (-not (Test-Path -LiteralPath $AssetRoot -PathType Container)) { - throw "AssetRoot not found: $AssetRoot" -} - -$exes = @( - Get-ChildItem -LiteralPath $AssetRoot -Recurse -File -Filter *.exe | - Sort-Object Name, FullName -) -if ($exes.Count -eq 0) { - throw "No .exe files under $AssetRoot" -} - -$bullets = New-Object System.Collections.Generic.List[string] -foreach ($exe in $exes) { - $hash = (Get-FileHash -LiteralPath $exe.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - $url = "https://www.virustotal.com/gui/file/$hash" - $bullets.Add("- [``$($exe.Name)``]($url)") - Write-Host ("VirusTotal link -> {0} => {1}" -f $exe.Name, $url) -} - -$section = "## VirusTotal`n`n" + ($bullets -join "`n") - -$view = gh release view $Tag --json body | ConvertFrom-Json -if ($LASTEXITCODE -ne 0) { - throw "gh release view $Tag failed" -} -$body = [string]$view.body -if ($null -eq $body) { - $body = "" -} -# Normalize newlines so override/replace is deterministic on Windows runners. -$body = $body -replace "`r`n", "`n" -replace "`r", "`n" - -# Drop any existing VirusTotal section (ours or crazy-max leftovers). -$body = [regex]::Replace( - $body, - '(?ms)^## VirusTotal\n(?:.*?)(?=^## |\n?> \[!IMPORTANT\]|\z)', - "", - 1 -) -$body = [regex]::Replace( - $body, - '(?m)\n*πŸ›‘ \[VirusTotal GitHub Action\][^\n]*(?:\n \* \[`[^\n]+)*', - "" -) -$body = [regex]::Replace( - $body, - '(?is)\n*
\n?\s*\s*πŸ›‘ VirusTotal analysis.*?
', - "" -) -$body = $body.TrimEnd() + "`n" - -$importantMarker = "> [!IMPORTANT]" -$idx = $body.IndexOf($importantMarker) -if ($idx -ge 0) { - $before = $body.Substring(0, $idx).TrimEnd() - $after = $body.Substring($idx).TrimStart("`n") - $body = $before + "`n`n" + $section + "`n`n" + $after -} -else { - $body = $body.TrimEnd() + "`n`n" + $section + "`n" -} -if (-not $body.EndsWith("`n")) { - $body += "`n" -} - -$notesFile = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-vt-notes-{0}.md" -f [guid]::NewGuid().ToString("n")) -try { - [System.IO.File]::WriteAllText($notesFile, $body, (New-Object System.Text.UTF8Encoding $false)) - gh release edit $Tag --notes-file $notesFile - if ($LASTEXITCODE -ne 0) { - throw "gh release edit $Tag failed" - } - Write-Host ("Updated release {0} VirusTotal section ({1} asset(s))." -f $Tag, $exes.Count) -} -finally { - if (Test-Path -LiteralPath $notesFile) { - Remove-Item -LiteralPath $notesFile -Force -ErrorAction SilentlyContinue - } -} diff --git a/buildtools/Default.isl b/buildtools/Default.isl new file mode 100644 index 00000000..b417cf91 --- /dev/null +++ b/buildtools/Default.isl @@ -0,0 +1,336 @@ +; *** Inno Setup version 5.5.3+ English messages *** +; +; To download user-contributed translations of this file, go to: +; http://www.jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=English +LanguageID=$0409 +LanguageCodePage=0 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +;DialogFontName= +;DialogFontSize=8 +;WelcomeFontName=Verdana +;WelcomeFontSize=12 +;TitleFontName=Arial +;TitleFontSize=29 +;CopyrightFontName=Arial +;CopyrightFontSize=8 + +[Messages] + +; *** Application titles +SetupAppTitle=Setup +SetupWindowTitle=Setup - %1 +UninstallAppTitle=Uninstall +UninstallAppFullTitle=%1 Uninstall + +; *** Misc. common +InformationTitle=Information +ConfirmTitle=Confirm +ErrorTitle=Error + +; *** SetupLdr messages +SetupLdrStartupMessage=This will install %1. Do you wish to continue? +LdrCannotCreateTemp=Unable to create a temporary file. Setup aborted +LdrCannotExecTemp=Unable to execute file in the temporary directory. Setup aborted + +; *** Startup error messages +LastErrorMessage=%1.%n%nError %2: %3 +SetupFileMissing=The file %1 is missing from the installation directory. Please correct the problem or obtain a new copy of the program. +SetupFileCorrupt=The setup files are corrupted. Please obtain a new copy of the program. +SetupFileCorruptOrWrongVer=The setup files are corrupted, or are incompatible with this version of Setup. Please correct the problem or obtain a new copy of the program. +InvalidParameter=An invalid parameter was passed on the command line:%n%n%1 +SetupAlreadyRunning=Setup is already running. +WindowsVersionNotSupported=This program does not support the version of Windows your computer is running. +WindowsServicePackRequired=This program requires %1 Service Pack %2 or later. +NotOnThisPlatform=This program will not run on %1. +OnlyOnThisPlatform=This program must be run on %1. +OnlyOnTheseArchitectures=This program can only be installed on versions of Windows designed for the following processor architectures:%n%n%1 +MissingWOW64APIs=The version of Windows you are running does not include functionality required by Setup to perform a 64-bit installation. To correct this problem, please install Service Pack %1. +WinVersionTooLowError=This program requires %1 version %2 or later. +WinVersionTooHighError=This program cannot be installed on %1 version %2 or later. +AdminPrivilegesRequired=You must be logged in as an administrator when installing this program. +PowerUserPrivilegesRequired=You must be logged in as an administrator or as a member of the Power Users group when installing this program. +SetupAppRunningError=Setup has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit. +UninstallAppRunningError=Uninstall has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit. + +; *** Misc. errors +ErrorCreatingDir=Setup was unable to create the directory "%1" +ErrorTooManyFilesInDir=Unable to create a file in the directory "%1" because it contains too many files + +; *** Setup common messages +ExitSetupTitle=Exit Setup +ExitSetupMessage=Setup is not complete. If you exit now, the program will not be installed.%n%nYou may run Setup again at another time to complete the installation.%n%nExit Setup? +AboutSetupMenuItem=&About Setup... +AboutSetupTitle=About Setup +AboutSetupMessage=%1 version %2%n%3%n%n%1 home page:%n%4 +AboutSetupNote= +TranslatorNote= + +; *** Buttons +ButtonBack=< &Back +ButtonNext=&Next > +ButtonInstall=&Install +ButtonOK=OK +ButtonCancel=Cancel +ButtonYes=&Yes +ButtonYesToAll=Yes to &All +ButtonNo=&No +ButtonNoToAll=N&o to All +ButtonFinish=&Finish +ButtonBrowse=&Browse... +ButtonWizardBrowse=B&rowse... +ButtonNewFolder=&Make New Folder + +; *** "Select Language" dialog messages +SelectLanguageTitle=Select Setup Language +SelectLanguageLabel=Select the language to use during the installation: + +; *** Common wizard text +ClickNext=Click Next to continue, or Cancel to exit Setup. +BeveledLabel= +BrowseDialogTitle=Browse For Folder +BrowseDialogLabel=Select a folder in the list below, then click OK. +NewFolderName=New Folder + +; *** "Welcome" wizard page +WelcomeLabel1=Welcome to the [name] Setup Wizard +WelcomeLabel2=This will install [name/ver] on your computer.%n%nIt is recommended that you close all other applications before continuing. + +; *** "Password" wizard page +WizardPassword=Password +PasswordLabel1=This installation is password protected. +PasswordLabel3=Please provide the password, then click Next to continue. Passwords are case-sensitive. +PasswordEditLabel=&Password: +IncorrectPassword=The password you entered is not correct. Please try again. + +; *** "License Agreement" wizard page +WizardLicense=License Agreement +LicenseLabel=Please read the following important information before continuing. +LicenseLabel3=Please read the following License Agreement. You must accept the terms of this agreement before continuing with the installation. +LicenseAccepted=I &accept the agreement +LicenseNotAccepted=I &do not accept the agreement + +; *** "Information" wizard pages +WizardInfoBefore=Information +InfoBeforeLabel=Please read the following important information before continuing. +InfoBeforeClickLabel=When you are ready to continue with Setup, click Next. +WizardInfoAfter=Information +InfoAfterLabel=Please read the following important information before continuing. +InfoAfterClickLabel=When you are ready to continue with Setup, click Next. + +; *** "User Information" wizard page +WizardUserInfo=User Information +UserInfoDesc=Please enter your information. +UserInfoName=&User Name: +UserInfoOrg=&Organization: +UserInfoSerial=&Serial Number: +UserInfoNameRequired=You must enter a name. + +; *** "Select Destination Location" wizard page +WizardSelectDir=Select Destination Location +SelectDirDesc=Where should [name] be installed? +SelectDirLabel3=Setup will install [name] into the following folder. +SelectDirBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse. +DiskSpaceMBLabel=At least [mb] MB of free disk space is required. +CannotInstallToNetworkDrive=Setup cannot install to a network drive. +CannotInstallToUNCPath=Setup cannot install to a UNC path. +InvalidPath=You must enter a full path with drive letter; for example:%n%nC:\APP%n%nor a UNC path in the form:%n%n\\server\share +InvalidDrive=The drive or UNC share you selected does not exist or is not accessible. Please select another. +DiskSpaceWarningTitle=Not Enough Disk Space +DiskSpaceWarning=Setup requires at least %1 KB of free space to install, but the selected drive only has %2 KB available.%n%nDo you want to continue anyway? +DirNameTooLong=The folder name or path is too long. +InvalidDirName=The folder name is not valid. +BadDirName32=Folder names cannot include any of the following characters:%n%n%1 +DirExistsTitle=Folder Exists +DirExists=The folder:%n%n%1%n%nalready exists. Would you like to install to that folder anyway? +DirDoesntExistTitle=Folder Does Not Exist +DirDoesntExist=The folder:%n%n%1%n%ndoes not exist. Would you like the folder to be created? + +; *** "Select Components" wizard page +WizardSelectComponents=Select Components +SelectComponentsDesc=Which components should be installed? +SelectComponentsLabel2=Select the components you want to install; clear the components you do not want to install. Click Next when you are ready to continue. +FullInstallation=Full installation +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=Compact installation +CustomInstallation=Custom installation +NoUninstallWarningTitle=Components Exist +NoUninstallWarning=Setup has detected that the following components are already installed on your computer:%n%n%1%n%nDeselecting these components will not uninstall them.%n%nWould you like to continue anyway? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceMBLabel=Current selection requires at least [mb] MB of disk space. + +; *** "Select Additional Tasks" wizard page +WizardSelectTasks=Select Additional Tasks +SelectTasksDesc=Which additional tasks should be performed? +SelectTasksLabel2=Select the additional tasks you would like Setup to perform while installing [name], then click Next. + +; *** "Select Start Menu Folder" wizard page +WizardSelectProgramGroup=Select Start Menu Folder +SelectStartMenuFolderDesc=Where should Setup place the program's shortcuts? +SelectStartMenuFolderLabel3=Setup will create the program's shortcuts in the following Start Menu folder. +SelectStartMenuFolderBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse. +MustEnterGroupName=You must enter a folder name. +GroupNameTooLong=The folder name or path is too long. +InvalidGroupName=The folder name is not valid. +BadGroupName=The folder name cannot include any of the following characters:%n%n%1 +NoProgramGroupCheck2=&Don't create a Start Menu folder + +; *** "Ready to Install" wizard page +WizardReady=Ready to Install +ReadyLabel1=Setup is now ready to begin installing [name] on your computer. +ReadyLabel2a=Click Install to continue with the installation, or click Back if you want to review or change any settings. +ReadyLabel2b=Click Install to continue with the installation. +ReadyMemoUserInfo=User information: +ReadyMemoDir=Destination location: +ReadyMemoType=Setup type: +ReadyMemoComponents=Selected components: +ReadyMemoGroup=Start Menu folder: +ReadyMemoTasks=Additional tasks: + +; *** "Preparing to Install" wizard page +WizardPreparing=Preparing to Install +PreparingDesc=Setup is preparing to install [name] on your computer. +PreviousInstallNotCompleted=The installation/removal of a previous program was not completed. You will need to restart your computer to complete that installation.%n%nAfter restarting your computer, run Setup again to complete the installation of [name]. +CannotContinue=Setup cannot continue. Please click Cancel to exit. +ApplicationsFound=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. +ApplicationsFound2=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. After the installation has completed, Setup will attempt to restart the applications. +CloseApplications=&Automatically close the applications +DontCloseApplications=&Do not close the applications +ErrorCloseApplications=Setup was unable to automatically close all applications. It is recommended that you close all applications using files that need to be updated by Setup before continuing. + +; *** "Installing" wizard page +WizardInstalling=Installing +InstallingLabel=Please wait while Setup installs [name] on your computer. + +; *** "Setup Completed" wizard page +FinishedHeadingLabel=Completing the [name] Setup Wizard +FinishedLabelNoIcons=Setup has finished installing [name] on your computer. +FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed icons. +ClickFinish=Click Finish to exit Setup. +FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now? +FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now? +ShowReadmeCheck=Yes, I would like to view the README file +YesRadio=&Yes, restart the computer now +NoRadio=&No, I will restart the computer later +; used for example as 'Run MyProg.exe' +RunEntryExec=Run %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=View %1 + +; *** "Setup Needs the Next Disk" stuff +ChangeDiskTitle=Setup Needs the Next Disk +SelectDiskLabel2=Please insert Disk %1 and click OK.%n%nIf the files on this disk can be found in a folder other than the one displayed below, enter the correct path or click Browse. +PathLabel=&Path: +FileNotInDir2=The file "%1" could not be located in "%2". Please insert the correct disk or select another folder. +SelectDirectoryLabel=Please specify the location of the next disk. + +; *** Installation phase messages +SetupAborted=Setup was not completed.%n%nPlease correct the problem and run Setup again. +EntryAbortRetryIgnore=Click Retry to try again, Ignore to proceed anyway, or Abort to cancel installation. + +; *** Installation status messages +StatusClosingApplications=Closing applications... +StatusCreateDirs=Creating directories... +StatusExtractFiles=Extracting files... +StatusCreateIcons=Creating shortcuts... +StatusCreateIniEntries=Creating INI entries... +StatusCreateRegistryEntries=Creating registry entries... +StatusRegisterFiles=Registering files... +StatusSavingUninstall=Saving uninstall information... +StatusRunProgram=Finishing installation... +StatusRestartingApplications=Restarting applications... +StatusRollback=Rolling back changes... + +; *** Misc. errors +ErrorInternal2=Internal error: %1 +ErrorFunctionFailedNoCode=%1 failed +ErrorFunctionFailed=%1 failed; code %2 +ErrorFunctionFailedWithMessage=%1 failed; code %2.%n%3 +ErrorExecutingProgram=Unable to execute file:%n%1 + +; *** Registry errors +ErrorRegOpenKey=Error opening registry key:%n%1\%2 +ErrorRegCreateKey=Error creating registry key:%n%1\%2 +ErrorRegWriteKey=Error writing to registry key:%n%1\%2 + +; *** INI errors +ErrorIniEntry=Error creating INI entry in file "%1". + +; *** File copying errors +FileAbortRetryIgnore=Click Retry to try again, Ignore to skip this file (not recommended), or Abort to cancel installation. +FileAbortRetryIgnore2=Click Retry to try again, Ignore to proceed anyway (not recommended), or Abort to cancel installation. +SourceIsCorrupted=The source file is corrupted +SourceDoesntExist=The source file "%1" does not exist +ExistingFileReadOnly=The existing file is marked as read-only.%n%nClick Retry to remove the read-only attribute and try again, Ignore to skip this file, or Abort to cancel installation. +ErrorReadingExistingDest=An error occurred while trying to read the existing file: +FileExists=The file already exists.%n%nWould you like Setup to overwrite it? +ExistingFileNewer=The existing file is newer than the one Setup is trying to install. It is recommended that you keep the existing file.%n%nDo you want to keep the existing file? +ErrorChangingAttr=An error occurred while trying to change the attributes of the existing file: +ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory: +ErrorReadingSource=An error occurred while trying to read the source file: +ErrorCopying=An error occurred while trying to copy a file: +ErrorReplacingExistingFile=An error occurred while trying to replace the existing file: +ErrorRestartReplace=RestartReplace failed: +ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory: +ErrorRegisterServer=Unable to register the DLL/OCX: %1 +ErrorRegSvr32Failed=RegSvr32 failed with exit code %1 +ErrorRegisterTypeLib=Unable to register the type library: %1 + +; *** Post-installation errors +ErrorOpeningReadme=An error occurred while trying to open the README file. +ErrorRestartingComputer=Setup was unable to restart the computer. Please do this manually. + +; *** Uninstaller messages +UninstallNotFound=File "%1" does not exist. Cannot uninstall. +UninstallOpenError=File "%1" could not be opened. Cannot uninstall +UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall +UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log +ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? +UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows. +OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges. +UninstallStatusLabel=Please wait while %1 is removed from your computer. +UninstalledAll=%1 was successfully removed from your computer. +UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually. +UninstalledAndNeedsRestart=To complete the uninstallation of %1, your computer must be restarted.%n%nWould you like to restart now? +UninstallDataCorrupted="%1" file is corrupted. Cannot uninstall + +; *** Uninstallation phase messages +ConfirmDeleteSharedFileTitle=Remove Shared File? +ConfirmDeleteSharedFile2=The system indicates that the following shared file is no longer in use by any programs. Would you like for Uninstall to remove this shared file?%n%nIf any programs are still using this file and it is removed, those programs may not function properly. If you are unsure, choose No. Leaving the file on your system will not cause any harm. +SharedFileNameLabel=File name: +SharedFileLocationLabel=Location: +WizardUninstalling=Uninstall Status +StatusUninstalling=Uninstalling %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp=Installing %1. +ShutdownBlockReasonUninstallingApp=Uninstalling %1. + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 version %2 +AdditionalIcons=Additional icons: +CreateDesktopIcon=Create a &desktop icon +CreateQuickLaunchIcon=Create a &Quick Launch icon +ProgramOnTheWeb=%1 on the Web +UninstallProgram=Uninstall %1 +LaunchProgram=Launch %1 +AssocFileExtension=&Associate %1 with the %2 file extension +AssocingFileExtension=Associating %1 with the %2 file extension... +AutoStartProgramGroupDescription=Startup: +AutoStartProgram=Automatically start %1 +AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway? diff --git a/buildtools/ISCmplr.dll b/buildtools/ISCmplr.dll new file mode 100644 index 00000000..c230d36c Binary files /dev/null and b/buildtools/ISCmplr.dll differ diff --git a/buildtools/ISPP.dll b/buildtools/ISPP.dll new file mode 100644 index 00000000..23e8d18f Binary files /dev/null and b/buildtools/ISPP.dll differ diff --git a/buildtools/Setup.e32 b/buildtools/Setup.e32 new file mode 100644 index 00000000..f6ecc793 Binary files /dev/null and b/buildtools/Setup.e32 differ diff --git a/buildtools/SetupLdr.e32 b/buildtools/SetupLdr.e32 new file mode 100644 index 00000000..95675506 Binary files /dev/null and b/buildtools/SetupLdr.e32 differ diff --git a/buildtools/WizModernImage.bmp b/buildtools/WizModernImage.bmp new file mode 100644 index 00000000..cb05a063 Binary files /dev/null and b/buildtools/WizModernImage.bmp differ diff --git a/buildtools/WizModernSmallImage.bmp b/buildtools/WizModernSmallImage.bmp new file mode 100644 index 00000000..63f42104 Binary files /dev/null and b/buildtools/WizModernSmallImage.bmp differ diff --git a/buildtools/iscc.exe b/buildtools/iscc.exe new file mode 100644 index 00000000..48815c8a Binary files /dev/null and b/buildtools/iscc.exe differ diff --git a/buildtools/islzma.dll b/buildtools/islzma.dll new file mode 100644 index 00000000..49395ada Binary files /dev/null and b/buildtools/islzma.dll differ diff --git a/buildtools/zip.exe b/buildtools/zip.exe new file mode 100644 index 00000000..286227a6 Binary files /dev/null and b/buildtools/zip.exe differ diff --git a/cli b/cli deleted file mode 160000 index 4006f30a..00000000 --- a/cli +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4006f30ab6885aa2461c094614cf2e98ffdba553 diff --git a/common b/common deleted file mode 160000 index 66bd493c..00000000 --- a/common +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 66bd493c8d647fe32ee843a764db00f9148bed1a diff --git a/examples/settings.txt b/examples/settings.txt new file mode 100644 index 00000000..83dda361 --- /dev/null +++ b/examples/settings.txt @@ -0,0 +1,4 @@ +root: C:\Users\Corey\AppData\Roaming\nvm +path: C:\Program Files\nodejs +arch: 64 +proxy: none diff --git a/installer/.gitignore b/installer/.gitignore deleted file mode 100644 index 4f368ccd..00000000 --- a/installer/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.* -!.gitignore -!.gitmodules -!.github -!.github/** -bin -dist diff --git a/installer/AGENTS.md b/installer/AGENTS.md deleted file mode 100644 index 99cdd967..00000000 --- a/installer/AGENTS.md +++ /dev/null @@ -1,8 +0,0 @@ -# Agent Development Guide - -A file for [guiding coding agents](https://agents.md/). - -## Setup - -1. Assure InnoSetup 6.7.1 or greater is installed and available. -1. Incorporate the skills in .github/skills. diff --git a/installer/InstallerReport.ps1 b/installer/InstallerReport.ps1 deleted file mode 100644 index 618c2097..00000000 --- a/installer/InstallerReport.ps1 +++ /dev/null @@ -1,447 +0,0 @@ -param( - [ValidateSet("run", "dialog", "submit", "budget")] - [string]$Action = "run", - - [string]$Exe = "", - - [string]$Arguments = "", - - [string]$ArgumentsFile = "", - - [string]$Arg1 = "", - - [string]$Arg2 = "", - - [string]$Arg3 = "", - - [string]$Arg4 = "", - - [string]$Arg5 = "", - - [string]$Arg6 = "", - - [int]$TimeoutSec = 180, - - [string]$Code = "", - - [string]$Message = "", - - [switch]$SoftFail, - - [switch]$RunAs, - - [string]$RollbackProductCode = "", - - [string]$RollbackCommandLog = "", - - [string]$ReportDirectory = "", - - [ValidateSet("step", "copy", "adopt", "elevated", "uninstall", "walk")] - [string]$BudgetKind = "step", - - [int]$VersionCount = 0 -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -function Get-InstallerStepBudgetSeconds { - param( - [ValidateSet("step", "copy", "adopt", "elevated", "uninstall", "walk")] - [string]$Kind, - [int]$Count = 0 - ) - # Budgets are hang limits. A slow machine should finish well under them. - if ($Kind -eq "adopt") { - return 900 - } - if ($Kind -eq "uninstall") { - return 600 - } - if ($Kind -eq "elevated") { - return 300 - } - if ($Kind -eq "copy") { - if ($Count -le 0) { - return 0 - } - return 300 - } - if ($Kind -eq "walk") { - if ($Count -lt 1) { - $Count = 1 - } - $budget = 120 * $Count - if ($budget -lt 600) { - return 600 - } - if ($budget -gt 1800) { - return 1800 - } - return $budget - } - return 180 -} - -function Get-InstallerReportDirectory { - param([string]$Override = "") - if (-not [string]::IsNullOrWhiteSpace($Override)) { - return $Override - } - $programData = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonApplicationData) - if ([string]::IsNullOrWhiteSpace($programData)) { - $programData = Join-Path $env:SystemDrive "ProgramData" - } - return Join-Path $programData "Author Software" -} - -function ConvertTo-RedactedInstallerText { - param( - [string]$Text, - [string]$UserName = "", - [string]$ComputerName = "", - [string]$DomainName = "", - [string]$DnsDomain = "" - ) - if ($null -eq $Text) { - return "" - } - $redacted = $Text - if ([string]::IsNullOrWhiteSpace($UserName)) { - $UserName = [Environment]::UserName - } - if ([string]::IsNullOrWhiteSpace($ComputerName)) { - $ComputerName = [Environment]::MachineName - } - if ([string]::IsNullOrWhiteSpace($DomainName)) { - $DomainName = [Environment]::UserDomainName - } - if ([string]::IsNullOrWhiteSpace($DnsDomain)) { - $DnsDomain = [Environment]::GetEnvironmentVariable("USERDNSDOMAIN") - if ([string]::IsNullOrWhiteSpace($DnsDomain)) { - try { - $DnsDomain = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName - } catch { - $DnsDomain = "" - } - } - } - - $redacted = [regex]::Replace($redacted, '(?i)([a-z][a-z0-9+.-]*://)[^/\s:@]+:[^/\s@]+@', '$1@') - $redacted = [regex]::Replace($redacted, '(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b', '') - $redacted = [regex]::Replace($redacted, '(?i)\\\\[^\\\s]+\\[^\\\s]+', '\\\') - $redacted = [regex]::Replace($redacted, '(?i)([A-Za-z]:\\Users\\)(?!(?:Public|Default User|Default|All Users)(?=\\|$))[^\\\r\n]+', '$1') - $redacted = [regex]::Replace($redacted, '(?i)OneDrive - [^\\\r\n]+', 'OneDrive - ') - - foreach ($token in @($DnsDomain, $DomainName, $ComputerName, $UserName)) { - if ([string]::IsNullOrWhiteSpace($token) -or $token.Length -lt 2) { - continue - } - $marker = '' - if ($token -eq $ComputerName) { - $marker = '' - } elseif (($token -eq $DomainName) -or ($token -eq $DnsDomain)) { - $marker = '' - } - $escaped = [regex]::Escape($token) - $redacted = [regex]::Replace($redacted, "(?i)(?') - return $redacted -} - -function Get-InstallerReportExcerpt { - param( - [string]$Text, - [int]$MaxChars = 32768 - ) - if ($null -eq $Text) { - return "" - } - if ($Text.Length -le $MaxChars) { - return $Text - } - return $Text.Substring($Text.Length - $MaxChars) -} - -function Write-InstallerFailureRecord { - param( - [Parameter(Mandatory = $true)] - [string]$FailureCode, - [Parameter(Mandatory = $true)] - [string]$FailureMessage, - [string]$Directory = "" - ) - $dir = Get-InstallerReportDirectory $Directory - New-Item -ItemType Directory -Force -Path $dir | Out-Null - $logPath = Join-Path $dir "nvm-setup.log" - $oneLine = ($FailureMessage -replace "[\r\n]+", " ").Trim() - $stamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - $line = "$stamp $FailureCode $oneLine" - [System.IO.File]::AppendAllText($logPath, $line + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) - $recordPath = Join-Path $dir "nvm-setup-failure.txt" - $body = $FailureCode + "`n" + $oneLine + "`n" + $logPath + "`n" - [System.IO.File]::WriteAllText($recordPath, $body, [System.Text.UTF8Encoding]::new($false)) - if (-not [string]::IsNullOrWhiteSpace($PSCommandPath) -and (Test-Path -LiteralPath $PSCommandPath)) { - Copy-Item -LiteralPath $PSCommandPath -Destination (Join-Path $dir "InstallerReport.ps1") -Force - } - return $logPath -} - -function Invoke-InstallerProductRollback { - param( - [string]$ProductCode, - [string]$CommandLog = "" - ) - if ([string]::IsNullOrWhiteSpace($ProductCode)) { - return - } - $rendered = "msiexec.exe /x $ProductCode /qn /norestart" - if (-not [string]::IsNullOrWhiteSpace($CommandLog)) { - [System.IO.File]::WriteAllText($CommandLog, $rendered) - return - } - $delayed = "ping -n 8 127.0.0.1 >nul & $rendered" - Start-Process -FilePath "$env:SystemRoot\System32\cmd.exe" -ArgumentList @("/c", $delayed) -WindowStyle Hidden | Out-Null -} - -function Invoke-InstallerTimedProcess { - param( - [Parameter(Mandatory = $true)] - [string]$FilePath, - [string]$ArgumentText = "", - [int]$Seconds = 60, - [string]$FailureCode = "", - [string]$FailureMessage = "", - [switch]$AllowNonZero, - [switch]$Elevate, - [string]$ProductCode = "", - [string]$ProductRollbackLog = "", - [string]$Directory = "" - ) - if ($Seconds -lt 1) { - $msg = $FailureMessage - if ([string]::IsNullOrWhiteSpace($msg)) { - $msg = "The setup step exceeded its time limit." - } - Write-InstallerFailureRecord -FailureCode $FailureCode -FailureMessage $msg -Directory $Directory | Out-Null - Invoke-InstallerProductRollback -ProductCode $ProductCode -CommandLog $ProductRollbackLog - return 1460 - } - - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = $FilePath - $psi.Arguments = $ArgumentText - if ($Elevate) { - $psi.UseShellExecute = $true - $psi.Verb = "RunAs" - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Normal - } else { - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - } - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $psi - try { - [void]$process.Start() - } catch { - if ($Elevate) { - return 1223 - } - throw - } - if (-not $process.WaitForExit($Seconds * 1000)) { - try { - $process.Kill() - } catch { - } - try { - [void]$process.WaitForExit(5000) - } catch { - } - $msg = $FailureMessage - if ([string]::IsNullOrWhiteSpace($msg)) { - $msg = "The setup step exceeded its time limit." - } - Write-InstallerFailureRecord -FailureCode $FailureCode -FailureMessage $msg -Directory $Directory | Out-Null - Invoke-InstallerProductRollback -ProductCode $ProductCode -CommandLog $ProductRollbackLog - return 1460 - } - $code = $process.ExitCode - if ((-not $AllowNonZero) -and $code -ne 0 -and $code -ge 8) { - $msg = $FailureMessage - if ([string]::IsNullOrWhiteSpace($msg)) { - $msg = "The setup step failed with exit code $code." - } - Write-InstallerFailureRecord -FailureCode $FailureCode -FailureMessage $msg -Directory $Directory | Out-Null - Invoke-InstallerProductRollback -ProductCode $ProductCode -CommandLog $ProductRollbackLog - } - return $code -} - -function Read-InstallerFailureRecord { - param([string]$Directory = "") - $candidates = @() - if (-not [string]::IsNullOrWhiteSpace($Directory)) { - $candidates += (Join-Path $Directory "nvm-setup-failure.txt") - } - $candidates += (Join-Path (Get-InstallerReportDirectory "") "nvm-setup-failure.txt") - $local = Join-Path $env:LOCALAPPDATA "Author Software\nvm-setup-failure.txt" - $candidates += $local - foreach ($path in $candidates) { - if (Test-Path -LiteralPath $path) { - $lines = [System.IO.File]::ReadAllLines($path) - $code = if ($lines.Length -gt 0) { $lines[0] } else { "" } - $msg = if ($lines.Length -gt 1) { $lines[1] } else { "" } - $log = if ($lines.Length -gt 2) { $lines[2] } else { "" } - return [pscustomobject]@{ Code = $code; Message = $msg; Log = $log } - } - } - return $null -} - -function Get-InstallerIssueReport { - param( - [string]$FailureCode, - [string]$FailureMessage, - [string]$LogPath - ) - $logText = "" - if (-not [string]::IsNullOrWhiteSpace($LogPath) -and (Test-Path -LiteralPath $LogPath)) { - $logText = [System.IO.File]::ReadAllText($LogPath) - } - $redacted = ConvertTo-RedactedInstallerText -Text $logText - $excerpt = Get-InstallerReportExcerpt -Text $redacted - $summary = @" -Install code: $FailureCode - -$FailureMessage - -autosubmission - -The redacted log is on the clipboard. Paste it below this line. - -"@ - return [pscustomobject]@{ - Summary = $summary.TrimEnd() - Excerpt = $excerpt - Clipboard = ($summary + "`r`n" + $excerpt).TrimEnd() - } -} - -function Open-InstallerIssue { - param([string]$Directory = "") - $record = Read-InstallerFailureRecord -Directory $Directory - if ($null -eq $record) { - throw "No installer failure record was found." - } - $report = Get-InstallerIssueReport -FailureCode $record.Code -FailureMessage $record.Message -LogPath $record.Log - Set-Clipboard -Value $report.Clipboard - $existing = "" - try { - $query = [uri]::EscapeDataString("repo:nvm-windows/nvm is:issue is:open `"$($record.Code)`" in:body") - $search = Invoke-RestMethod -Uri "https://api.github.com/search/issues?q=$query&per_page=1" -Headers @{ "User-Agent" = "nvm-windows-setup" } - if ($search.total_count -gt 0 -and $search.items) { - $existing = [string]$search.items[0].html_url - } - } catch { - $existing = "" - } - if (-not [string]::IsNullOrWhiteSpace($existing)) { - Start-Process $existing | Out-Null - return $existing - } - $title = [uri]::EscapeDataString("$($record.Code) installer failure") - $body = [uri]::EscapeDataString($report.Summary) - $labels = [uri]::EscapeDataString("autosubmission,$($record.Code)") - $url = "https://github.com/nvm-windows/nvm/issues/new?title=$title&body=$body&labels=$labels" - Start-Process $url | Out-Null - return $url -} - -function Show-InstallerFailureDialog { - param([string]$Directory = "") - $script:InstallerDialogDirectory = $Directory - $record = Read-InstallerFailureRecord -Directory $Directory - if ($null -eq $record) { - return - } - Add-Type -AssemblyName System.Windows.Forms - Add-Type -AssemblyName System.Drawing - $form = New-Object System.Windows.Forms.Form - $form.Text = "NVM for Windows Setup" - $form.Width = 560 - $form.Height = 240 - $form.StartPosition = "CenterScreen" - $form.FormBorderStyle = "FixedDialog" - $form.MaximizeBox = $false - $form.MinimizeBox = $false - $label = New-Object System.Windows.Forms.Label - $label.Left = 16 - $label.Top = 16 - $label.Width = 510 - $label.Height = 72 - $label.Text = "$($record.Code)`r`n$($record.Message)`r`n`r`nLog:" - $path = New-Object System.Windows.Forms.TextBox - $path.Left = 16 - $path.Top = 96 - $path.Width = 510 - $path.ReadOnly = $true - $path.Text = $record.Log - $close = New-Object System.Windows.Forms.Button - $close.Text = "Close" - $close.Left = 340 - $close.Top = 150 - $close.Width = 80 - $close.DialogResult = [System.Windows.Forms.DialogResult]::Cancel - $submit = New-Object System.Windows.Forms.Button - $submit.Text = "Submit Issue" - $submit.Left = 430 - $submit.Top = 150 - $submit.Width = 100 - $submit.Add_Click({ - try { - Open-InstallerIssue -Directory $script:InstallerDialogDirectory | Out-Null - } catch { - [System.Windows.Forms.MessageBox]::Show($_.Exception.Message, "NVM for Windows Setup") - } - $form.Close() - }) - $form.Controls.AddRange(@($label, $path, $close, $submit)) - $form.AcceptButton = $close - $form.CancelButton = $close - [void]$form.ShowDialog() -} - -if ($MyInvocation.InvocationName -eq ".") { - return -} - -switch ($Action) { - "budget" { - Write-Output (Get-InstallerStepBudgetSeconds -Kind $BudgetKind -Count $VersionCount) - exit 0 - } - "dialog" { - Show-InstallerFailureDialog -Directory $ReportDirectory - exit 0 - } - "submit" { - Open-InstallerIssue -Directory $ReportDirectory | Out-Null - exit 0 - } - default { - $argumentText = $Arguments - if (-not [string]::IsNullOrWhiteSpace($ArgumentsFile)) { - $argumentText = [System.IO.File]::ReadAllText($ArgumentsFile) - } - if ([string]::IsNullOrWhiteSpace($argumentText)) { - $parts = @($Arg1, $Arg2, $Arg3, $Arg4, $Arg5, $Arg6) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - $argumentText = ($parts | ForEach-Object { - if (($_ -match '\s') -and ($_ -notmatch '"')) { '"{0}"' -f $_ } else { $_ } - }) -join ' ' - } - $exitCode = Invoke-InstallerTimedProcess -FilePath $Exe -ArgumentText $argumentText -Seconds $TimeoutSec -FailureCode $Code -FailureMessage $Message -AllowNonZero:$SoftFail -Elevate:$RunAs -ProductCode $RollbackProductCode -ProductRollbackLog $RollbackCommandLog -Directory $ReportDirectory - exit $exitCode - } -} diff --git a/installer/LICENSE b/installer/LICENSE deleted file mode 100644 index c106f114..00000000 --- a/installer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Author Software Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/installer/NVMWindows.Events.man.template b/installer/NVMWindows.Events.man.template deleted file mode 100644 index 0060008a..00000000 --- a/installer/NVMWindows.Events.man.template +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/installer/README.md b/installer/README.md deleted file mode 100644 index 1c69e4bf..00000000 --- a/installer/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Installer - -The installer requires [InnoSetup](https://jrsoftware.org/isdl.php) 6.7.1 or above. - diff --git a/installer/assets/left-banner.png b/installer/assets/left-banner.png deleted file mode 100644 index 6f0df63c..00000000 Binary files a/installer/assets/left-banner.png and /dev/null differ diff --git a/installer/assets/nvm.ico b/installer/assets/nvm.ico deleted file mode 100644 index 6d94a052..00000000 Binary files a/installer/assets/nvm.ico and /dev/null differ diff --git a/installer/build-installer.bat b/installer/build-installer.bat deleted file mode 100644 index 5ef2d5e7..00000000 --- a/installer/build-installer.bat +++ /dev/null @@ -1,182 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -rem Build NVM for Windows installer using Inno Setup. -rem Usage: -rem build-installer.bat -rem build-installer.bat 2.0.0-alpha.1 -rem build-installer.bat 2.0.0-alpha.1 AuthorSoftware.NVMWindows -rem build-installer.bat 2.0.0-alpha.1 AuthorSoftware.NVMWindows 40078385-F676-4C61-9A9C-F9028599D6D3 amd64 -rem build-installer.bat 2.0.0-alpha.1 AuthorSoftware.NVMWindows 40078385-F676-4C61-9A9C-F9028599D6D3 amd64 (all args optional; defaults from manifest) - -set "SCRIPT_DIR=%~dp0" -set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" -set "ROOT_DIR=%SCRIPT_DIR%\.." -set "DIST_DIR=%ROOT_DIR%\.dist" -set "SETUP_ISS=%SCRIPT_DIR%\setup.iss" -set "MANIFEST=%ROOT_DIR%\cli\src\manifest.json" -if not exist "%MANIFEST%" set "MANIFEST=%ROOT_DIR%\nvm\src\manifest.json" -set "ARCHITECTURE=%~4" -if "%ARCHITECTURE%"=="" set "ARCHITECTURE=amd64" -set "LICENSE_SRC=%ROOT_DIR%\LICENSE" -set "LICENSE_DST=%SCRIPT_DIR%\LICENSE" - -if not exist "%SETUP_ISS%" ( - echo ERROR: setup.iss not found at "%SETUP_ISS%" - exit /b 1 -) - -if not exist "%MANIFEST%" ( - echo ERROR: manifest.json not found at "%MANIFEST%" - exit /b 1 -) - -if "%~1"=="" ( - for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).version"`) do set "VERSION=%%V" -) else ( - set "VERSION=%~1" -) - -if "%~2"=="" ( - for /f "usebackq delims=" %%A in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).appUserModelId"`) do set "APP_USER_MODEL_ID=%%A" -) else ( - set "APP_USER_MODEL_ID=%~2" -) - -if "%~3"=="" ( - for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).appId"`) do set "APP_ID=%%I" -) else ( - set "APP_ID=%~3" -) - -for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).author"`) do set "PUBLISHER=%%P" -for /f "usebackq delims=" %%N in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).name"`) do set "ALIAS=%%N" -for /f "usebackq delims=" %%N in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).appLabel"`) do set "APP_LABEL=%%N" -for /f "usebackq delims=" %%O in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).appOrgLabel"`) do set "APP_ORG_LABEL=%%O" -for /f "usebackq delims=" %%D in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).description"`) do set "DESCRIPTION=%%D" -for /f "usebackq delims=" %%U in (`powershell -NoProfile -Command "(Get-Content -Raw '%MANIFEST%' | ConvertFrom-Json).appUrl"`) do set "APP_URL=%%U" - -if "%VERSION%"=="" ( - echo ERROR: Could not determine version. - exit /b 1 -) - -for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "$v = '%VERSION%'; if ($v -match '^(?\d+)\.(?\d+)\.(?\d+)(?:-(?:\d+\.\d+\.\d+-)?[A-Za-z]+\.(?\d+))?$') { $rev = if ($matches['rev']) { [int]$matches['rev'] } else { 0 }; '{0}.{1}.{2}.{3}' -f [int]$matches['major'], [int]$matches['minor'], [int]$matches['patch'], $rev } else { Write-Error ('Unsupported version format: ' + $v); exit 1 }"`) do set "VERSION_INFO_VERSION=%%V" - -if "%VERSION_INFO_VERSION%"=="" ( - echo ERROR: Could not derive numeric VersionInfoVersion from "%VERSION%". - exit /b 1 -) - -if "%APP_USER_MODEL_ID%"=="" ( - echo ERROR: Could not determine AppUserModelId. - exit /b 1 -) - -if "%APP_ID%"=="" ( - echo ERROR: Could not determine AppId. - exit /b 1 -) - -if "%PUBLISHER%"=="" ( - echo ERROR: Could not determine Publisher from manifest author. - exit /b 1 -) - -if "%ALIAS%"=="" ( - echo ERROR: Could not determine Alias from manifest name. - exit /b 1 -) - -if "%APP_LABEL%"=="" ( - echo ERROR: Could not determine AppLabel. - exit /b 1 -) - -if "%APP_ORG_LABEL%"=="" ( - echo ERROR: Could not determine AppOrgLabel. - exit /b 1 -) - -if "%DESCRIPTION%"=="" ( - echo ERROR: Could not determine Description. - exit /b 1 -) - -if "%APP_URL%"=="" ( - echo ERROR: Could not determine AppUrl. - exit /b 1 -) - -set "ISCC=" -if exist "%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" set "ISCC=%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" -if not defined ISCC if exist "%ProgramFiles%\Inno Setup 6\ISCC.exe" set "ISCC=%ProgramFiles%\Inno Setup 6\ISCC.exe" -if not defined ISCC if exist "%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" set "ISCC=%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" - -if not defined ISCC ( - for /f "usebackq delims=" %%I in (`where iscc 2^>nul`) do ( - set "ISCC=%%I" - goto :iscc_found - ) -) - -:iscc_found -if not defined ISCC ( - echo ERROR: ISCC.exe not found. Install Inno Setup 6 or add ISCC to PATH. - exit /b 1 -) - -if exist "%LICENSE_SRC%" ( - copy /Y "%LICENSE_SRC%" "%LICENSE_DST%" >nul -) - -if not exist "%DIST_DIR%" mkdir "%DIST_DIR%" >nul 2>nul - -set "ARCH_TOKEN=%ARCHITECTURE%" -if /I "%ARCHITECTURE%"=="amd64" set "ARCH_TOKEN=x64" -set "OUTPUT_BASENAME=%ALIAS%-%VERSION%-%ARCH_TOKEN%-setup" -set "BUILD_OUTPUT_BASENAME=%OUTPUT_BASENAME%" -set "OUTPUT_FILE=%DIST_DIR%\%BUILD_OUTPUT_BASENAME%.exe" -set "LEGACY_OUTPUT_FILE=%DIST_DIR%\%ALIAS%-%VERSION%-amd64-setup.exe" - -if /I not "%OUTPUT_FILE%"=="%LEGACY_OUTPUT_FILE%" ( - if exist "%LEGACY_OUTPUT_FILE%" del /q "%LEGACY_OUTPUT_FILE%" -) - -if exist "%OUTPUT_FILE%" ( - set "OUTPUT_PREP=UNKNOWN" - for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "$path = '%OUTPUT_FILE%'; try { Remove-Item -LiteralPath $path -Force -ErrorAction Stop; 'REMOVED' } catch { 'LOCKED' }"`) do set "OUTPUT_PREP=%%S" - - if /I "!OUTPUT_PREP!"=="LOCKED" ( - for /f "usebackq delims=" %%T in (`powershell -NoProfile -Command "Get-Date -Format 'yyyyMMddHHmmss'"`) do set "OUTPUT_TIMESTAMP=%%T" - set "BUILD_OUTPUT_BASENAME=!OUTPUT_BASENAME!-!OUTPUT_TIMESTAMP!" - set "OUTPUT_FILE=%DIST_DIR%\!BUILD_OUTPUT_BASENAME!.exe" - echo WARNING: Canonical output is locked. Building to "!OUTPUT_FILE!" instead. - ) -) - -echo Building installer version %VERSION% -echo Using Alias: %ALIAS% -echo Using AppLabel: %APP_LABEL% -echo Using AppOrgLabel: %APP_ORG_LABEL% -echo Using AppUserModelId: %APP_USER_MODEL_ID% -echo Using AppId: %APP_ID% -echo Using Publisher: %PUBLISHER% -echo Using AppUrl: %APP_URL% -echo Using Architecture: %ARCHITECTURE% -echo Using VersionInfoVersion: %VERSION_INFO_VERSION% -echo Using ISCC: %ISCC% - -pushd "%SCRIPT_DIR%" -"%ISCC%" /DVersion=%VERSION% /DVersionInfoVersion=%VERSION_INFO_VERSION% /DAppUserModelId=%APP_USER_MODEL_ID% /DAppId=%APP_ID% "/DPublisher=%PUBLISHER%" "/DOrgLabel=%APP_ORG_LABEL%" "/DAlias=%ALIAS%" "/DName=%APP_LABEL%" "/DDescription=%DESCRIPTION%" "/DURL=%APP_URL%" /DArchitecture=%ARCHITECTURE% /DOutputFileName=%BUILD_OUTPUT_BASENAME% "%SETUP_ISS%" -set "BUILD_EXIT=%ERRORLEVEL%" -popd - -if not "%BUILD_EXIT%"=="0" ( - echo Build failed with exit code %BUILD_EXIT%. - exit /b %BUILD_EXIT% -) - -echo Build succeeded. -echo Output: %OUTPUT_FILE% -exit /b 0 diff --git a/installer/build.ps1 b/installer/build.ps1 deleted file mode 100644 index 30109830..00000000 --- a/installer/build.ps1 +++ /dev/null @@ -1,241 +0,0 @@ -param( - [switch]$StopOnError, - [switch]$RequireSync, - [ValidateSet("amd64", "arm64")] - [string]$Architecture -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$installerRoot = Split-Path -Parent $MyInvocation.MyCommand.Path -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $installerRoot "..")) -$statuses = New-Object System.Collections.Generic.List[object] - -if ([string]::IsNullOrWhiteSpace($Architecture)) { - if ([System.Environment]::Is64BitProcess) { - if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - $Architecture = "arm64" - } else { - $Architecture = "amd64" - } - } else { - $Architecture = "amd64" - } -} - -Write-Host "Building installer pipeline for architecture: $Architecture" - -function Add-Status { - param( - [string]$Component, - [string]$Step, - [string]$Status, - [string]$Message, - [double]$Seconds - ) - - $statuses.Add([PSCustomObject]@{ - Component = $Component - Step = $Step - Status = $Status - Seconds = [Math]::Round($Seconds, 2) - Message = $Message - }) -} - -function Invoke-Step { - param( - [string]$Component, - [string]$Step, - [string]$WorkingDirectory, - [scriptblock]$Action - ) - - Write-Host "`n[$Component] $Step" - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $locationPushed = $false - - try { - Push-Location $WorkingDirectory - $locationPushed = $true - & $Action - if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) { - throw "Command exited with code $LASTEXITCODE" - } - - $sw.Stop() - Add-Status -Component $Component -Step $Step -Status "OK" -Message "Completed" -Seconds $sw.Elapsed.TotalSeconds - Write-Host "[$Component] OK" - return $true - } - catch { - $sw.Stop() - Add-Status -Component $Component -Step $Step -Status "FAILED" -Message $_.Exception.Message -Seconds $sw.Elapsed.TotalSeconds - Write-Host "[$Component] FAILED: $($_.Exception.Message)" -ForegroundColor Red - - if ($StopOnError) { - throw - } - - return $false - } - finally { - if ($locationPushed) { - Pop-Location - } - } -} - -function Build-Cli { - $componentRoot = Join-Path $repoRoot "cli" - $workingDir = $componentRoot - - if (-not (Test-Path (Join-Path $componentRoot "manifest.json")) -and - (Test-Path (Join-Path $componentRoot "src\manifest.json"))) { - $workingDir = Join-Path $componentRoot "src" - } - - Invoke-Step -Component "cli" -Step "qgo exec prebuild" -WorkingDirectory $workingDir -Action { - qgo exec prebuild - } - - Invoke-Step -Component "cli" -Step "qgo build --no-cache" -WorkingDirectory $workingDir -Action { - $env:GOARCH = if ($Architecture -eq "arm64") { "arm64" } else { "amd64" } - qgo build --no-cache - } -} - -function Build-Shim { - $componentRoot = Join-Path $repoRoot "shim" - Invoke-Step -Component "shim" -Step "build.ps1" -WorkingDirectory $componentRoot -Action { - .\build.ps1 -Architecture $Architecture - } -} - -function Build-Sync { - $componentRoot = Join-Path $repoRoot "sync" - $srcDir = Join-Path $componentRoot "src" - $assetsDir = Join-Path $componentRoot "assets" - $buildDir = Join-Path $componentRoot "build\build" - if (-not (Test-Path $buildDir)) { - $buildDir = Join-Path $componentRoot "build" - } - - $syncAvailable = - (Test-Path $componentRoot -PathType Container) -and - (Test-Path $srcDir -PathType Container) -and - (Test-Path $assetsDir -PathType Container) -and - (Test-Path $buildDir -PathType Container) - - if (-not $syncAvailable) { - $msg = "Sync component is unavailable (missing sync source/build directories)." - if ($RequireSync) { - Add-Status -Component "sync" -Step "availability check" -Status "FAILED" -Message "$msg Pass -RequireSync:`$false (or omit it) to allow skipping." -Seconds 0 - Write-Host "[sync] FAILED: $msg" -ForegroundColor Red - if ($StopOnError) { - throw $msg - } - return $false - } - - Add-Status -Component "sync" -Step "availability check" -Status "SKIPPED" -Message "$msg Skipping sync build." -Seconds 0 - Write-Host "[sync] SKIPPED: $msg" -ForegroundColor Yellow - return $true - } - - $ok = $true - - if (-not (Invoke-Step -Component "sync" -Step "qgo build (build tool)" -WorkingDirectory $buildDir -Action { - $env:GOARCH = if ($Architecture -eq "arm64") { "arm64" } else { "amd64" } - qgo build - })) { - $ok = $false - } - - if (-not (Invoke-Step -Component "sync" -Step "qgo exec prebuild (app)" -WorkingDirectory (Join-Path $componentRoot "src") -Action { - qgo exec prebuild - })) { - $ok = $false - } - - if (-not (Invoke-Step -Component "sync" -Step "qgo build --no-cache (app)" -WorkingDirectory (Join-Path $componentRoot "src") -Action { - $env:GOARCH = if ($Architecture -eq "arm64") { "arm64" } else { "amd64" } - qgo build --no-cache - })) { - $ok = $false - } - - if (-not (Invoke-Step -Component "sync" -Step "sync-build.exe build ./assets -o ../bin/.sync" -WorkingDirectory $componentRoot -Action { - .\sync-build.exe build ./assets -o ../bin/.sync - })) { - $ok = $false - } - - return $ok -} - -function Build-Updater { - $updaterRoot = Join-Path $installerRoot "updater" - Invoke-Step -Component "installer" -Step "updater\build.ps1" -WorkingDirectory $updaterRoot -Action { - .\build.ps1 -Architecture $Architecture - } -} - -function Build-Installer { - $manifestPath = Join-Path $repoRoot "cli\src\manifest.json" - if (-not (Test-Path $manifestPath)) { - $manifestPath = Join-Path $repoRoot "nvm\src\manifest.json" - } - - if (-not (Test-Path $manifestPath)) { - throw "installer manifest.json was not found" - } - - $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json - $installerVersion = $manifest.version - $installerAumid = $manifest.appUserModelId - $installerAppId = $manifest.appId - - if ([string]::IsNullOrWhiteSpace($installerVersion) -or [string]::IsNullOrWhiteSpace($installerAumid) -or [string]::IsNullOrWhiteSpace($installerAppId)) { - throw "installer manifest values are required before invoking build-installer.bat" - } - - Invoke-Step -Component "installer" -Step "build-installer.bat" -WorkingDirectory $installerRoot -Action { - cmd /c .\build-installer.bat $installerVersion $installerAumid $installerAppId $Architecture - } -} - -function Build-EventProvider { - $step = Join-Path $repoRoot "build\steps\Build-EventProvider.ps1" - Invoke-Step -Component "eventlog" -Step "Build-EventProvider.ps1" -WorkingDirectory $repoRoot -Action { - & $step -Architecture $Architecture - } -} - -try { - [void](Build-Cli) - [void](Build-EventProvider) - [void](Build-Shim) - [void](Build-Sync) - [void](Build-Updater) - [void](Build-Installer) -} -catch { - Write-Host "Build aborted: $($_.Exception.Message)" -ForegroundColor Red -} - -Write-Host "`nBuild Status Summary" -Write-Host "====================" -$statuses | Format-Table -AutoSize Component, Step, Status, Seconds, Message - -$failedSteps = @($statuses | Where-Object { $_.Status -eq "FAILED" }) - -if ($failedSteps.Count -eq 0) { - Write-Host "`nOverall Status: SUCCESS" -ForegroundColor Green - exit 0 -} - -$failedComponents = @($failedSteps | Select-Object -ExpandProperty Component -Unique) -Write-Host "`nOverall Status: FAILED ($($failedComponents.Count) component(s): $([string]::Join(', ', $failedComponents)))" -ForegroundColor Red -exit 1 diff --git a/installer/installtimeout.iss b/installer/installtimeout.iss deleted file mode 100644 index 6fae1230..00000000 --- a/installer/installtimeout.iss +++ /dev/null @@ -1,144 +0,0 @@ -function GetTickCount: Cardinal; - external 'GetTickCount@kernel32.dll stdcall'; - -function InstallerReportScript: String; -begin - Result := ExpandConstant('{tmp}\InstallerReport.ps1'); - if not FileExists(Result) then - Result := ExpandConstant('{app}\utils\InstallerReport.ps1'); -end; - -procedure BeginStepBudget(const Code, Message: String; TimeoutSec: Integer); -begin - BudgetCode := Code; - BudgetMessage := Message; - StepBudgetArmed := TimeoutSec > 0; - if StepBudgetArmed then - BudgetDeadline := GetTickCount + Cardinal(TimeoutSec) * 1000 - else - BudgetDeadline := 0; -end; - -function WalkTimeoutSec(const VersionCount: Integer): Integer; -begin - if VersionCount < 1 then - Result := 600 - else - Result := VersionCount * 120; - if Result < 600 then - Result := 600; - if Result > 1800 then - Result := 1800; -end; - -function RemainingBudgetSec: Integer; -var - LeftMs: Integer; -begin - if not StepBudgetArmed then - begin - Result := 180; - Exit; - end; - LeftMs := Integer(BudgetDeadline - GetTickCount); - if LeftMs < 1000 then - Result := 0 - else - Result := LeftMs div 1000; -end; - -procedure ShowInstallerFailureDialog(); -var - ScriptPath: String; - ResultCode: Integer; -begin - ScriptPath := InstallerReportScript; - if not FileExists(ScriptPath) then - Exit; - Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptPath + '" -Action dialog', - '', - SW_SHOWNORMAL, - ewWaitUntilTerminated, - ResultCode - ); -end; - -function ExecStep( - const FileName, Params, Code, Message: String; - TimeoutSec: Integer; - SoftFail, RunAs: Boolean; - var ResultCode: Integer -): Boolean; -var - ScriptPath, ArgPath, CommandLine, Flags: String; - LaunchCode: Integer; -begin - Result := False; - ResultCode := 0; - if InstallerHardFailed then - begin - ResultCode := 1460; - Exit; - end; - if SuppressStepFailure then - begin - Result := Exec(FileName, Params, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); - Exit; - end; - if TimeoutSec <= 0 then - begin - ResultCode := 1460; - InstallerHardFailed := True; - Exit; - end; - - ScriptPath := InstallerReportScript; - if not FileExists(ScriptPath) then - begin - Result := Exec(FileName, Params, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); - Exit; - end; - - ArgPath := ExpandConstant('{tmp}\nvm-step-args.txt'); - SaveStringToFile(ArgPath, Params, False); - Flags := ''; - if SoftFail then - Flags := Flags + ' -SoftFail'; - if RunAs then - Flags := Flags + ' -RunAs'; - CommandLine := - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptPath + '"' + - ' -Action run -Exe "' + FileName + '" -ArgumentsFile "' + ArgPath + '"' + - ' -TimeoutSec ' + IntToStr(TimeoutSec) + - ' -Code ' + Code + - ' -Message "' + Message + '"' + - Flags; - if not Exec(ExpandConstant('{cmd}'), CommandLine, '', SW_HIDE, ewWaitUntilTerminated, LaunchCode) then - begin - ResultCode := LaunchCode; - Exit; - end; - - ResultCode := LaunchCode; - if LaunchCode = 1460 then - begin - InstallerHardFailed := True; - Exit; - end; - Result := True; -end; - -procedure ScheduleInstallerRollback(); -var - Uninstaller, CommandLine: String; - ResultCode: Integer; -begin - Uninstaller := ExpandConstant('{app}\unins000.exe'); - if not FileExists(Uninstaller) then - Exit; - CommandLine := - '/C ping -n 8 127.0.0.1 >nul & "' + Uninstaller + '" /SILENT'; - Exec(ExpandConstant('{cmd}'), CommandLine, '', SW_HIDE, ewNoWait, ResultCode); -end; diff --git a/installer/officialnode.iss b/installer/officialnode.iss deleted file mode 100644 index 29a25d22..00000000 --- a/installer/officialnode.iss +++ /dev/null @@ -1,651 +0,0 @@ -{ Official Node.js adopt / drop. Included from setup.iss [Code]. } -{ DeferOfficialNodeOnV2Upgrade is declared by the host script. A 2.0.0+ upgrade leaves official Node.js installed. } - -function OfficialNodeParam(): String; -var - Raw: String; -begin - Raw := LowerCase(Trim(ExpandConstant('{param:OFFICIALNODE}'))); - if (Raw = 'adopt') or (Raw = 'drop') then - Result := Raw - else - Result := 'adopt'; -end; - -function IsNvmManagedPath(const PathValue: String): Boolean; -var - N: String; - AppRoot: String; - Home: String; - InstallRoot: String; -begin - Result := False; - N := LowerCase(NormalizePath(PathValue)); - if N = '' then - Exit; - - // app constant is not initialized in InitializeWizard. Program root is the real LOCALAPPDATA path. - AppRoot := LowerCase(NormalizePath(GetRealProgramRoot(''))); - Home := LowerCase(NormalizePath(GetEnv('NVM_HOME'))); - InstallRoot := LowerCase(NormalizePath(GetInstallRoot(''))); - - if (AppRoot <> '') and ((N = AppRoot) or (Pos(AppRoot + '\', N) = 1)) then - begin - Result := True; - Exit; - end; - if (Home <> '') and (Pos('author software\nvm', Home) > 0) and - ((N = Home) or (Pos(Home + '\', N) = 1)) then - begin - Result := True; - Exit; - end; - if (InstallRoot <> '') and ((N = InstallRoot) or (Pos(InstallRoot + '\', N) = 1)) then - Result := True; -end; - -function PathIsReparsePoint(const DirPath: String): Boolean; -var - FindRec: TFindRec; -begin - Result := False; - if FindFirst(DirPath, FindRec) then - begin - Result := (FindRec.Attributes and FILE_ATTRIBUTE_REPARSE_POINT) <> 0; - FindClose(FindRec); - end; -end; - -function OfficialNodeLooksLikeInstall(const DirPath: String): Boolean; -begin - { A v1 NVM_SYMLINK directory symlink is not the nodejs.org product. The v1 cleanup removes the link. } - Result := - (not IsNvmManagedPath(DirPath)) and - (not PathIsReparsePoint(DirPath)) and - FileExists(AddBackslash(DirPath) + 'node.exe'); -end; - -function StripNodeFileVersion(const FileVersion: String): String; -begin - Result := Trim(FileVersion); - if Result = '' then - Exit; - StringChangeEx(Result, ',', '.', True); - { 22.20.0.0 -> 22.20.0 } - if (Length(Result) > 2) and (Copy(Result, Length(Result) - 1, 2) = '.0') and - (Pos('.', Copy(Result, 1, Length(Result) - 2)) > 0) then - Result := Copy(Result, 1, Length(Result) - 2); - while (Length(Result) > 0) and ((Result[1] = 'v') or (Result[1] = 'V')) do - Result := Copy(Result, 2, MaxInt); -end; - -function OfficialNodeVersionDirName(): String; -var - V: String; -begin - V := Trim(OfficialNodeVersion); - if V = '' then - V := 'unknown'; - if (Length(V) > 0) and ((V[1] = 'v') or (V[1] = 'V')) then - Result := V - else - Result := 'v' + V; -end; - -procedure MeasureTreeBytes(const RootDir: String; var Bytes: Int64); -var - FindRec: TFindRec; - CurrentPath: String; - Chunk: Int64; -begin - if not FindFirst(AddBackslash(RootDir) + '*', FindRec) then - Exit; - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') then - begin - CurrentPath := AddBackslash(RootDir) + FindRec.Name; - if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then - begin - if (FindRec.Attributes and FILE_ATTRIBUTE_REPARSE_POINT) = 0 then - MeasureTreeBytes(CurrentPath, Bytes); - end - else - begin - Chunk := FindRec.SizeHigh; - Chunk := (Chunk shl 32) + FindRec.SizeLow; - Bytes := Bytes + Chunk; - end; - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -procedure MeasureGlobalNpmModules(var ModuleCount: Integer; var Bytes: Int64); -var - RootDir: String; - FindRec: TFindRec; - NameLower: String; -begin - ModuleCount := 0; - Bytes := 0; - RootDir := ExpandConstant('{userappdata}\npm\node_modules'); - if not DirExists(RootDir) then - Exit; - - MeasureTreeBytes(RootDir, Bytes); - - if not FindFirst(AddBackslash(RootDir) + '*', FindRec) then - Exit; - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') and - ((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) then - begin - NameLower := LowerCase(FindRec.Name); - if (NameLower <> '.bin') and (NameLower <> '.package-lock.json') and - ((FindRec.Attributes and FILE_ATTRIBUTE_HIDDEN) = 0) then - ModuleCount := ModuleCount + 1; - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -function FormatOfficialNodeSize(const Bytes: Int64): String; -var - Kb: Extended; - Mb: Extended; -begin - if Bytes <= 0 then - begin - Result := '0 MB'; - Exit; - end; - Kb := Bytes / 1024.0; - if Kb < 1024.0 then - begin - Result := IntToStr(Round(Kb)) + ' KB'; - Exit; - end; - Mb := Kb / 1024.0; - if Mb < 10.0 then - Result := Format('%.1f MB', [Mb]) - else - Result := IntToStr(Round(Mb)) + ' MB'; -end; - -function OfficialNodeSummary(): String; -begin - Result := - 'Node.js ' + OfficialNodeVersion + ' detected with ' + - IntToStr(OfficialNodeModuleCount) + ' global modules (' + - FormatOfficialNodeSize(OfficialNodeModuleSize) + ')'; -end; - -function TrySetOfficialNodeFromDir(const DirPath, VersionHint, UninstallString, ProductCode: String; IsMsi: Boolean): Boolean; -var - Dir: String; - Ver: String; - FileVer: String; -begin - Result := False; - Dir := NormalizePath(DirPath); - if not OfficialNodeLooksLikeInstall(Dir) then - Exit; - - Ver := Trim(VersionHint); - FileVer := ''; - if FileExists(AddBackslash(Dir) + 'node.exe') then - begin - if GetVersionNumbersString(AddBackslash(Dir) + 'node.exe', FileVer) then - FileVer := StripNodeFileVersion(FileVer) - else - FileVer := ''; - end; - if Ver = '' then - Ver := FileVer; - Ver := StripNodeFileVersion(Ver); - if Ver = '' then - Ver := 'unknown'; - - OfficialNodeDetected := True; - OfficialNodePath := Dir; - OfficialNodeVersion := Ver; - OfficialNodeUninstallString := Trim(UninstallString); - OfficialNodeProductCode := Trim(ProductCode); - OfficialNodeIsMsi := IsMsi; - MeasureGlobalNpmModules(OfficialNodeModuleCount, OfficialNodeModuleSize); - Result := True; -end; - -function ProductCodeFromKeyName(const KeyName: String): String; -var - K: String; -begin - Result := ''; - K := Trim(KeyName); - if (Length(K) = 38) and (K[1] = '{') and (K[38] = '}') then - Result := K; -end; - -function UninstallLooksLikeMsi(const UninstallString: String; const WindowsInstaller: Cardinal; const KeyName: String): Boolean; -var - LowerUninstall: String; -begin - Result := False; - if WindowsInstaller = 1 then - begin - Result := True; - Exit; - end; - if ProductCodeFromKeyName(KeyName) <> '' then - begin - Result := True; - Exit; - end; - LowerUninstall := LowerCase(UninstallString); - if (Pos('msiexec', LowerUninstall) > 0) then - Result := True; -end; - -function TryOfficialNodeFromUninstallRoot(const Root: Integer; const UninstallRoot: String): Boolean; -var - SubKeys: TArrayOfString; - I: Integer; - KeyName: String; - FullSubKey: String; - DisplayName: String; - InstallLocation: String; - DisplayVersion: String; - UninstallString: String; - WindowsInstaller: Cardinal; - IsMsi: Boolean; -begin - Result := False; - if not RegGetSubkeyNames(Root, UninstallRoot, SubKeys) then - Exit; - - for I := 0 to GetArrayLength(SubKeys) - 1 do - begin - KeyName := SubKeys[I]; - FullSubKey := UninstallRoot + '\' + KeyName; - DisplayName := ''; - if not RegQueryStringValue(Root, FullSubKey, 'DisplayName', DisplayName) then - Continue; - if CompareText(Trim(DisplayName), 'Node.js') <> 0 then - Continue; - - InstallLocation := ''; - DisplayVersion := ''; - UninstallString := ''; - WindowsInstaller := 0; - RegQueryStringValue(Root, FullSubKey, 'InstallLocation', InstallLocation); - RegQueryStringValue(Root, FullSubKey, 'DisplayVersion', DisplayVersion); - RegQueryStringValue(Root, FullSubKey, 'UninstallString', UninstallString); - RegQueryDWordValue(Root, FullSubKey, 'WindowsInstaller', WindowsInstaller); - IsMsi := UninstallLooksLikeMsi(UninstallString, WindowsInstaller, KeyName); - - if Trim(InstallLocation) = '' then - Continue; - if TrySetOfficialNodeFromDir(InstallLocation, DisplayVersion, UninstallString, ProductCodeFromKeyName(KeyName), IsMsi) then - begin - if OfficialNodeProductCode = '' then - OfficialNodeProductCode := ProductCodeFromKeyName(KeyName); - Result := True; - Exit; - end; - end; -end; - -procedure DetectOfficialNode(); -var - InstallPath: String; - DisplayVersion: String; -begin - OfficialNodeDetected := False; - OfficialNodePath := ''; - OfficialNodeVersion := ''; - OfficialNodeProductCode := ''; - OfficialNodeUninstallString := ''; - OfficialNodeIsMsi := False; - OfficialNodeModuleCount := 0; - OfficialNodeModuleSize := 0; - OfficialNodeCopyFailed := False; - OfficialNodeDropFailed := False; - OfficialNodeDropError := ''; - OfficialNodeAction := OfficialNodeParam(); - - if TryOfficialNodeFromUninstallRoot(HKLM64, 'Software\Microsoft\Windows\CurrentVersion\Uninstall') then - Exit; - if TryOfficialNodeFromUninstallRoot(HKLM32, 'Software\Microsoft\Windows\CurrentVersion\Uninstall') then - Exit; - if TryOfficialNodeFromUninstallRoot(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall') then - Exit; - if TryOfficialNodeFromUninstallRoot(HKLM, 'Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall') then - Exit; - - InstallPath := ''; - DisplayVersion := ''; - if RegQueryStringValue(HKLM64, 'SOFTWARE\Node.js', 'InstallPath', InstallPath) or - RegQueryStringValue(HKLM, 'SOFTWARE\Node.js', 'InstallPath', InstallPath) then - begin - RegQueryStringValue(HKLM64, 'SOFTWARE\Node.js', 'Version', DisplayVersion); - if DisplayVersion = '' then - RegQueryStringValue(HKLM, 'SOFTWARE\Node.js', 'Version', DisplayVersion); - if TrySetOfficialNodeFromDir(InstallPath, DisplayVersion, '', '', True) then - Exit; - end; - - if TrySetOfficialNodeFromDir(ExpandConstant('{pf}\nodejs'), '', '', '', False) then - Exit; - if TrySetOfficialNodeFromDir(GetEnv('ProgramW6432') + '\nodejs', '', '', '', False) then - Exit; - if TrySetOfficialNodeFromDir(ExpandConstant('{pf32}\nodejs'), '', '', '', False) then - Exit; -end; - -function SelectedOfficialNodeAction(): String; -begin - Result := OfficialNodeAction; - if OfficialNodePage = nil then - Exit; - if OfficialNodePage.Values[1] then - Result := 'drop' - else - Result := 'adopt'; -end; - -procedure CreateOfficialNodePage(); -var - Description: String; -begin - OfficialNodePage := nil; - if not OfficialNodeDetected then - Exit; - if DeferOfficialNodeOnV2Upgrade then - Exit; - - Description := - OfficialNodeSummary() + #13#10 + - 'Location: ' + OfficialNodePath + #13#10#13#10 + - 'The existing Node.js installation overrides NVM for Windows and must be removed. Removing it may require elevated privileges.'; - - OfficialNodePage := CreateInputOptionPage( - wpLicense, - 'Existing Node.js', - OfficialNodeSummary(), - Description, - True, - False - ); - OfficialNodePage.Add('Adopt β€” copy into NVM and uninstall the existing installation'); - OfficialNodePage.Add('Drop β€” uninstall the existing installation (do not copy)'); - - if OfficialNodeAction = 'drop' then - OfficialNodePage.Values[1] := True - else - OfficialNodePage.Values[0] := True; -end; - -function AdoptOfficialNodeVersion(): Boolean; -var - DestDir: String; - SourceDir: String; - UserModules: String; - DestModules: String; - CopiedCount: Integer; - ProgressPage: TOutputProgressWizardPage; - ResultCode: Integer; - UseVer: String; -begin - Result := False; - SourceDir := OfficialNodePath; - DestDir := AddBackslash(GetInstallRoot('')) + OfficialNodeVersionDirName(); - BeginStepBudget('NVM4106', 'Adopting official Node.js exceeded 15 minutes.', 900); - AppendInstallLog('AdoptOfficialNode: source=' + SourceDir + ' dest=' + DestDir); - - if FileExists(AddBackslash(DestDir) + 'node.exe') then - begin - AppendInstallLog('AdoptOfficialNode: destination already has node.exe'); - Result := True; - end - else - begin - CopiedCount := 0; - ProgressPage := CreateOutputProgressPage( - 'Adopting official Node.js', - OfficialNodeSummary() - ); - ProgressPage.SetProgress(0, 1); - ProgressPage.Show; - try - if not CopyTreeWithProgress(SourceDir, DestDir, ProgressPage, CopiedCount, 1, 'Node.js ' + OfficialNodeVersion) then - begin - OfficialNodeCopyFailed := True; - AppendInstallLogWarn('AdoptOfficialNode: copy failed'); - Exit; - end; - finally - ProgressPage.Hide; - end; - Result := FileExists(AddBackslash(DestDir) + 'node.exe'); - if not Result then - begin - OfficialNodeCopyFailed := True; - AppendInstallLogWarn('AdoptOfficialNode: node.exe missing after copy'); - Exit; - end; - end; - - UserModules := Trim(ExpandConstant('{param:OFFICIALNODEMODULES}')); - if UserModules = '' then - UserModules := ExpandConstant('{userappdata}\npm\node_modules'); - DestModules := AddBackslash(DestDir) + 'node_modules'; - if DirExists(UserModules) then - begin - CopiedCount := 0; - if not CopyTreeWithProgress(UserModules, DestModules, nil, CopiedCount, 1, '') then - AppendInstallLogWarn('AdoptOfficialNode: global module overlay failed (version copy kept)'); - end; - - UseVer := OfficialNodeVersion; - if not ExecStep( - ExpandConstant('{app}\{#Alias}.exe'), - 'use ' + UseVer, - 'NVM4106', - 'Adopting official Node.js exceeded 15 minutes.', - RemainingBudgetSec, - True, - False, - ResultCode - ) then - AppendInstallLogWarn('AdoptOfficialNode: nvm use failed to start') - else if ResultCode <> 0 then - AppendInstallLogWarn('AdoptOfficialNode: nvm use exit=' + IntToStr(ResultCode)) - else - AppendInstallLog('AdoptOfficialNode: nvm use ' + UseVer); -end; - -function UninstallResultOk(const Started: Boolean; const ResultCode: Integer): Boolean; -begin - Result := Started and ((ResultCode = 0) or (ResultCode = 3010) or (ResultCode = 1641)); -end; - -procedure NoteUninstallFailure(const Started: Boolean; const ResultCode: Integer; const FailureText: String); -begin - OfficialNodeDropFailed := True; - if (not Started) and (ResultCode = 1223) then - OfficialNodeDropError := 'The admin prompt was canceled.' - else if not Started then - OfficialNodeDropError := 'Could not start the uninstaller (system error ' + IntToStr(ResultCode) + ').' - else - OfficialNodeDropError := FailureText + ' (exit ' + IntToStr(ResultCode) + ').'; - AppendInstallLogWarn('DropOfficialNode: ' + OfficialNodeDropError); -end; - -function OfficialNodeUninstallWasRecorded(const FileName, Args: String): Boolean; -var - LogPath: String; -begin - LogPath := Trim(ExpandConstant('{param:OFFICIALNODELOG}')); - Result := LogPath <> ''; - if Result then - SaveStringToFile(LogPath, FileName + ' ' + Args + #13#10, False); -end; - -function DropOfficialNodeInstall(): Boolean; -var - ResultCode: Integer; - Args: String; - Ok: Boolean; -begin - Result := False; - AppendInstallLog('DropOfficialNode: msi=' + IntToStr(Integer(OfficialNodeIsMsi)) + ' product=' + OfficialNodeProductCode); - - if OfficialNodeIsMsi and (OfficialNodeProductCode <> '') then - begin - Args := '/x ' + OfficialNodeProductCode + ' /qn /norestart'; - if OfficialNodeUninstallWasRecorded(ExpandConstant('{sys}\msiexec.exe'), Args) then - begin - Result := True; - Exit; - end; - Ok := ExecStep( - ExpandConstant('{sys}\msiexec.exe'), - Args, - 'NVM4115', - 'Uninstalling official Node.js exceeded 10 minutes.', - 600, - False, - True, - ResultCode - ); - if not UninstallResultOk(Ok, ResultCode) then - begin - NoteUninstallFailure(Ok, ResultCode, 'Silent MSI uninstall failed'); - Exit; - end; - Result := True; - Exit; - end; - - if OfficialNodeIsMsi and (OfficialNodeUninstallString <> '') and - (Pos('msiexec', LowerCase(OfficialNodeUninstallString)) > 0) then - begin - Args := OfficialNodeUninstallString; - StringChangeEx(Args, '/I', '/X', True); - StringChangeEx(Args, '/i', '/x', True); - if Pos('/qn', LowerCase(Args)) = 0 then - Args := Args + ' /qn /norestart'; - if OfficialNodeUninstallWasRecorded(ExpandConstant('{cmd}'), '/C ' + Args) then - begin - Result := True; - Exit; - end; - Ok := ExecStep(ExpandConstant('{cmd}'), '/C ' + Args, 'NVM4115', 'Uninstalling official Node.js exceeded 10 minutes.', 600, False, True, ResultCode); - if not UninstallResultOk(Ok, ResultCode) then - begin - NoteUninstallFailure(Ok, ResultCode, 'MSI uninstall string failed'); - Exit; - end; - Result := True; - Exit; - end; - - if Trim(OfficialNodeUninstallString) <> '' then - begin - if OfficialNodeUninstallWasRecorded(ExpandConstant('{cmd}'), '/C ' + OfficialNodeUninstallString) then - begin - Result := True; - Exit; - end; - Ok := ExecStep(ExpandConstant('{cmd}'), '/C ' + OfficialNodeUninstallString, 'NVM4115', 'Uninstalling official Node.js exceeded 10 minutes.', 600, False, True, ResultCode); - if not UninstallResultOk(Ok, ResultCode) then - begin - NoteUninstallFailure(Ok, ResultCode, 'Official uninstall failed'); - Exit; - end; - Result := True; - Exit; - end; - - OfficialNodeDropFailed := True; - OfficialNodeDropError := 'No uninstall command was found for official Node.js.'; - AppendInstallLogWarn('DropOfficialNode: ' + OfficialNodeDropError); -end; - -procedure ExitProcess(uExitCode: Cardinal); - external 'ExitProcess@kernel32.dll stdcall'; - -procedure AbortInstallBecauseOfficialNodeRemains(); -var - MessageText: String; -begin - if InstallerHardFailed then - Exit; - MessageText := - 'The existing Node.js installation could not be removed.' + #13#10#13#10 + - OfficialNodeDropError + #13#10#13#10 + - 'Exit the NVM for Windows installer, remove the existing Node.js version, then run the installer again.' + #13#10#13#10 + - 'Node.js is still at:' + #13#10 + - OfficialNodePath + #13#10#13#10 + - 'Removing it may require elevated privileges.'; - AppendInstallLogWarn('ApplyOfficialNodeChoice: ' + OfficialNodeDropError); - if not WizardSilent then - begin - MsgBox(MessageText, mbError, MB_OK); - Abort; - end; - { ssPostInstall is too late for Abort to change the process exit code. } - ExitProcess(1); -end; - -procedure ApplyOfficialNodeChoice(); -begin - if not OfficialNodeDetected then - Exit; - - if DeferOfficialNodeOnV2Upgrade then - begin - AppendInstallLog('ApplyOfficialNodeChoice: left in place for a 2.x upgrade at ' + OfficialNodePath); - Exit; - end; - - OfficialNodeAction := SelectedOfficialNodeAction(); - AppendInstallLog('ApplyOfficialNodeChoice: action=' + OfficialNodeAction + ' path=' + OfficialNodePath); - - if OfficialNodeAction = 'drop' then - DropOfficialNodeInstall() - else - begin - if not AdoptOfficialNodeVersion() then - OfficialNodeCopyFailed := True; - if InstallerHardFailed then - Exit; - DropOfficialNodeInstall(); - end; - - if OfficialNodeDropFailed then - AbortInstallBecauseOfficialNodeRemains(); -end; - -procedure WarnIfOfficialNodeIncomplete(); -var - MessageText: String; -begin - if WizardSilent then - Exit; - - if OfficialNodeCopyFailed then - begin - MessageText := - OfficialNodeSummary() + #13#10#13#10 + - 'NVM could not copy this version. The existing Node.js installation was removed.' + #13#10#13#10 + - 'Install that version later with nvm install.'; - MsgBox(MessageText, mbInformation, MB_OK); - end; -end; diff --git a/installer/setup.iss b/installer/setup.iss deleted file mode 100644 index 254124d3..00000000 --- a/installer/setup.iss +++ /dev/null @@ -1,5070 +0,0 @@ -#ifndef Version - #error "Version is required. Build with /DVersion=x.y.z" -#endif - -#ifndef AppId - #error "AppId is required. Build with /DAppId=your-guid" -#endif - -#ifndef Alias - #error "Alias is required. Build with /DAlias=your-alias" -#endif - -#ifndef Name - #error "Name is required. Build with /DName=your-app-name" -#endif - -#ifndef Description - #error "Description is required. Build with /DDescription=your-description" -#endif - -#ifndef URL - #error "URL is required. Build with /DURL=https://example.com" -#endif - -#ifndef Publisher - #error "Publisher is required. Build with /DPublisher=your.publisher" -#endif - -#ifndef OrgLabel - #error "OrgLabel is required. Build with /DOrgLabel=your-org-label" -#endif - -#ifndef Architecture - #define Architecture "amd64" -#endif - -#ifndef VersionInfoVersion - #error "VersionInfoVersion is required. Build with /DVersionInfoVersion=a.b.c.d" -#endif - -#define ProjectRoot SourcePath -#define Icon "assets\\" + Alias + ".ico" -#define RegistryKey "Software\\" + OrgLabel + "\\Preferences\\" + Alias -#define SyncTaskName Name + " Sync" -#define SyncExeBuildPath "..\\bin\\utils\\sync.exe" -#define IconFullPath AddBackslash(ProjectRoot) + Icon -#define IconErrorMessage "Icon file not found: " + IconFullPath -#ifndef OutputFileName - #if Architecture == "amd64" - #define OutputFileName Alias + "-" + Version + "-x64-setup" - #else - #define OutputFileName Alias + "-" + Version + "-" + Architecture + "-setup" - #endif -#endif - -#pragma message IconErrorMessage - -#ifnexist IconFullPath - #error IconErrorMessage -#endif - -[Setup] -PrivilegesRequired=lowest -CloseApplications=yes -CloseApplicationsFilter=*.exe -AppId={#AppId} -AppName={#Name} -AppVersion={#Version} -AppVerName={#Name} {#Version} -AppPublisher={#Publisher} -AppPublisherURL={#URL} -AppSupportURL={#URL} -AppUpdatesURL={#URL} -; Refuse amd64 installer on native ARM64 (Prism) and arm64 installer on x64. -; x64compatible alone allows ARM64 Windows to run the amd64 setup under emulation. -#if Architecture == "arm64" -ArchitecturesAllowed=arm64 -ArchitecturesInstallIn64BitMode=arm64 -#else -ArchitecturesAllowed=x64compatible and not arm64 -ArchitecturesInstallIn64BitMode=x64compatible -#endif -; {localappdata} follows the shell-folder API and can resolve to a mapped A: drive. -; LOCALAPPDATA is the profile path Windows and nvm already use. -DefaultDirName={code:GetRealProgramRoot} -UsePreviousAppDir=no -LicenseFile={#ProjectRoot}\LICENSE -OutputDir={#ProjectRoot}\..\.dist\ -OutputBaseFilename={#OutputFileName} -; OutputBaseFilename={#Alias}-setup-{#Version}.{#GetDateTimeString('yyyymmddHHnnss', '', '')} -SetupIconFile={#ProjectRoot}\{#Icon} -UninstallDisplayName={#Name} -UninstallDisplayIcon={app}\.icons\{#Alias}.ico -WizardImageFile={#ProjectRoot}\assets\left-banner.png -WizardStyle=classic -Compression=lzma -SolidCompression=yes -; Hide the directory page. /DIR mismatch: silent install aborts (Application event NVM4100 / EventId 4100); interactive OK ignores /DIR, Cancel aborts (ForceProgramRootDirectory). -DisableDirPage=yes -DisableProgramGroupPage=yes -DisableReadyPage=no -VersionInfoVersion={#VersionInfoVersion} -VersionInfoCompany={#Publisher} -VersionInfoDescription={#Description} -VersionInfoProductName={#Name} -VersionInfoProductTextVersion={#Version} - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Messages] -WelcomeLabel1=[name] Setup Wizard -ReadyLabel2b= -UninstalledMost=%1 was successfully removed from your computer. -#if Architecture == "arm64" -WindowsVersionNotSupported=This NVM for Windows installer is built for ARM64. This computer is not native ARM64.%n%nDownload the amd64 (x64) installer instead, then try again. -#else -WindowsVersionNotSupported=This NVM for Windows installer is built for amd64 (x64). This computer appears to be ARM64 or another unsupported architecture.%n%nDownload the ARM64 installer instead, then try again. -#endif - -[Registry] -; Register nvm protocol -Root: HKCU; Subkey: "Software\Classes\{#Alias}"; ValueType: string; ValueName: ""; ValueData: "URL:{#Alias}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#Alias}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#Alias}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\.icons\{#Alias}.ico"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#Alias}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#Alias}.exe"" ""%1"""; Flags: uninsdeletekey - -; Register AUMID for Windows notification center integration -Root: HKCU; Subkey: "Software\Classes\AppUserModelId\AuthorSoftware.NVMWindows"; ValueType: string; ValueName: "DisplayName"; ValueData: "{#Name}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\AppUserModelId\AuthorSoftware.NVMWindows"; ValueType: string; ValueName: "IconUri"; ValueData: "{app}\.icons\{#Alias}.ico"; Flags: uninsdeletekey - -; Runtime settings under HKCU\{#RegistryKey} -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "Enabled"; ValueData: "1"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "InstallRoot"; ValueData: "{code:GetInstallRoot}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "ActiveVersion"; ValueData: "{code:GetActiveVersionRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "OperatingMode"; ValueData: "{code:GetOperatingModeRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: multisz; ValueName: "MirrorNode"; ValueData: "{code:GetNodeMirrorRegistryValue}"; Flags: uninsdeletevalue; Check: ShouldWriteNodeMirror -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: multisz; ValueName: "MirrorNpm"; ValueData: "{code:GetNpmMirrorRegistryValue}"; Flags: uninsdeletevalue; Check: ShouldWriteNpmMirror -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: multisz; ValueName: "AutoDetect"; ValueData: "{code:GetAutoDetectRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "DisableAutoUpgrade"; ValueData: "0"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AutoUse"; ValueData: "{code:GetAutoUseRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "CacheDownloads"; ValueData: "{code:GetCacheDownloadsRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AutoInstall"; ValueData: "{code:GetAutoInstallRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AutoInstallPrompt"; ValueData: "{code:GetAutoInstallPromptRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AllowInsecureDownloads"; ValueData: "{code:GetAllowInsecureDownloadsRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AnnounceNVM"; ValueData: "1"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: dword; ValueName: "AnnounceAuthor"; ValueData: "1"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "LastUpdateCheck"; ValueData: "{code:GetCurrentDateTimeRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "LastNewsCheck"; ValueData: "{code:GetCurrentDateTimeRegistryValue}"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "ReleaseFeedURL"; ValueData: "https://updates.nvm-windows.com/releases"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "NewsFeedURL"; ValueData: "https://updates.nvm-windows.com/news"; Flags: uninsdeletevalue -Root: HKCU; Subkey: "{#RegistryKey}"; ValueType: string; ValueName: "PackageManagerMismatchAction"; ValueData: "error"; Flags: uninsdeletevalue - -; Per-user environment variables for runtime discovery. -Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "NVM_HOME"; ValueData: "{app}"; Flags: uninsdeletevalue - -[Dirs] -Name: "{code:GetInstallRoot}"; Flags: uninsneveruninstall -Name: "{app}\.sync"; Attribs: hidden -Name: "{app}\.shim"; Attribs: hidden -Name: "{app}\.link"; Attribs: hidden -Name: "{app}\.icons"; Attribs: hidden -Name: "{app}\utils"; Attribs: hidden - -[Files] -Source: "..\bin\nvm.exe"; DestDir: "{app}"; Flags: ignoreversion -; ETW provider assets for nvm --register-eventlog (wevtutil). Must sit next to nvm.exe. -Source: "..\bin\NVMWindows.Events.man"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\bin\NVMWindows.Events.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\bin\utils\*"; DestDir: "{app}\utils"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "..\bin\.shim\*"; DestDir: "{app}\.shim"; Flags: ignoreversion recursesubdirs createallsubdirs -#ifexist SyncExeBuildPath -Source: "..\bin\.sync\*"; DestDir: "{app}\.sync"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist -#endif -Source: "updater\bin\nvm-upgrader.exe"; DestDir: "{tmp}"; Flags: ignoreversion deleteafterinstall -Source: "{#ProjectRoot}\{#Icon}"; DestDir: "{app}\.icons"; DestName: "{#Alias}.ico"; Flags: ignoreversion -Source: "InstallerReport.ps1"; DestDir: "{tmp}"; Flags: deleteafterinstall -Source: "InstallerReport.ps1"; DestDir: "{app}\utils"; Flags: ignoreversion - -[Icons] -Name: "{userprograms}\{#Name}"; Filename: "{app}\{#Alias}.exe"; IconFilename: "{app}\.icons\{#Alias}.ico"; Comment: "Node Version Manager"; AppUserModelID: "AuthorSoftware.NVMWindows" -Name: "{userprograms}\{#Name}\Sync"; Filename: "{app}\utils\sync.exe"; IconFilename: "{app}\.icons\{#Alias}.ico"; Comment: "Background synchronization service"; AppUserModelID: "AuthorSoftware.NVMWindows" - -[Code] -var - NodeStoragePage: TInputDirWizardPage; - StoragePermissionsPage: TInputOptionWizardPage; - StoragePermissionsPathLabel: TNewStaticText; - OperatingModePage: TInputOptionWizardPage; - PreferencesPage: TInputOptionWizardPage; - EmailPage: TWizardPage; - EmailEdit: TEdit; - EmailLabel: TLabel; - EmailPlaceholder: String; - EmailFirstFocus: Boolean; - DetectedActiveVersion: String; - PreviousInstallRoot: String; - MigrationPerformed: Boolean; - MigrationSource: String; - MigrationDest: String; - InstallCompleted: Boolean; - RemoveLegacyTasks: Boolean; - IsPreV2Upgrade: Boolean; - IsExistingV2Upgrade: Boolean; - IsConcreteLegacyUpgrade: Boolean; - LegacyInstallDir: String; - LegacySettingsLoaded: Boolean; - LegacySettingsRoot: String; - LegacySettingsPath: String; - LegacySettingsNodeMirror: String; - LegacySettingsNpmMirror: String; - LegacyNodeMirrorFound: Boolean; - LegacyNpmMirrorFound: Boolean; - InstallLogPath: String; - InstallLogBuffer: String; - InstallLogHasIssues: Boolean; - InstallerHardFailed: Boolean; - SuppressStepFailure: Boolean; - StepBudgetArmed: Boolean; - BudgetDeadline: Cardinal; - BudgetCode: String; - BudgetMessage: String; - DeferOfficialNodeOnV2Upgrade: Boolean; - SkippedSymlinkLogPath: String; - SkippedSymlinkLogBuffer: String; - SkippedSymlinkCount: Integer; - WizardDefaultInstallRoot: String; - WizardDefaultUseLinkMode: Boolean; - WizardDefaultCacheDownloads: Boolean; - WizardDefaultAutoDetect: Boolean; - WizardDefaultAutoInstall: Boolean; - WizardDefaultAutoInstallPrompt: Boolean; - WizardDefaultRequireTls: Boolean; - LegacyCleanupWarnings: String; - UninstallInstallRoot: String; - UninstallAppRoot: String; - UninstallLeftovers: String; - UninstallDelayedCleanupScheduled: Boolean; - UninstallSelfExe: String; - { 0=Harden, 1=AppData, 2=Keep/degraded } - AclStorageChoice: Integer; - SilentForcedAppDataFrom: String; - RuntimeACLDegraded: Boolean; - ShimFinalizeIncomplete: Boolean; - OfficialNodePage: TInputOptionWizardPage; - OfficialNodeDetected: Boolean; - OfficialNodePath: String; - OfficialNodeVersion: String; - OfficialNodeProductCode: String; - OfficialNodeUninstallString: String; - OfficialNodeIsMsi: Boolean; - OfficialNodeModuleCount: Integer; - OfficialNodeModuleSize: Int64; - OfficialNodeAction: String; - OfficialNodeCopyFailed: Boolean; - OfficialNodeDropFailed: Boolean; - OfficialNodeDropError: String; - ReadyEventNote: TNewStaticText; - ReadySummaryTop: TNewStaticText; - ReadyPrefCheck: array[0..4] of TNewCheckBox; - ReadySummaryBottom: TNewStaticText; - ReadySummaryCertified: TNewLinkLabel; - -const - WM_SETTINGCHANGE = $001A; - SMTO_ABORTIFHUNG = 2; - WindowsAppsUninstallRoot = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'; - SyncTaskName = '{#SyncTaskName}'; - -#include "installtimeout.iss" - -function SendMessageTimeoutW(hWnd: HWND; Msg: UINT; wParam: UINT; lParam: String; - fuFlags: UINT; uTimeout: UINT; var lpdwResult: DWORD): DWORD; - external 'SendMessageTimeoutW@user32.dll stdcall'; - -// Inno's localappdata constant uses the shell-folder API. A mapped A: drive -// can make that return A:\Users\... instead of the real profile. LOCALAPPDATA does not. -function GetRealLocalAppData(Param: String): String; -var - Profile: String; -begin - Result := RemoveBackslashUnlessRoot(Trim(GetEnv('LOCALAPPDATA'))); - if Result <> '' then - Exit; - - Profile := RemoveBackslashUnlessRoot(Trim(GetEnv('USERPROFILE'))); - if Profile <> '' then - Result := Profile + '\AppData\Local'; -end; - -function GetRealProgramRoot(Param: String): String; -begin - Result := GetRealLocalAppData('') + '\{#OrgLabel}\{#Alias}'; -end; - -function GetRealInstallRoot(Param: String): String; -begin - Result := GetRealProgramRoot('') + '\installs'; -end; - -function NormalizePath(const PathValue: String): String; forward; -procedure SplitPathString(const PathStr: String; var Segments: TArrayOfString); forward; -function ExpandPathSegment(const Segment: String): String; forward; -function GetInstallRootForUninstall(Param: String): String; forward; -procedure ForceCloseNvmProcessesOnUninstall(); forward; -function BuildNvmProcessManagementScript( - const AppRoot, InstallRoot, OutputFile, Mode, ExcludeProcessPath: String -): String; forward; -function FormatBlockingProcessLine(const Line: String): String; forward; - -procedure BroadcastEnvironmentChange(); -var - MsgResult: DWORD; -begin - MsgResult := 0; - SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment', - SMTO_ABORTIFHUNG, 5000, MsgResult); -end; - -procedure ResetInstallLog(); -begin - InstallLogPath := ExpandConstant('{app}\install.log'); - InstallLogBuffer := 'NVM for Windows install log' + #13#10; - InstallLogHasIssues := False; -end; - -procedure AppendInstallLog(const Message: String); -begin - if InstallLogPath = '' then - Exit; - InstallLogBuffer := InstallLogBuffer + Message + #13#10; -end; - -procedure AppendInstallLogWarn(const Message: String); -begin - InstallLogHasIssues := True; - AppendInstallLog(Message); -end; - -// Writes a classic Application log Error via PowerShell. No custom source registration. -// Temp file is only a transport for the message (deleted after); Event Viewer is the durable record. -procedure WriteApplicationEventError(EventId: Integer; const Message: String); -var - TempFile: String; - ResultCode: Integer; - Cmd: String; -begin - if Trim(Message) = '' then - Exit; - - TempFile := ExpandConstant('{tmp}\nvm-setup-appevent.txt'); - DeleteFile(TempFile); - SaveStringToFile(TempFile, Message, False); - - Cmd := - '-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "' + - 'try { Write-EventLog -LogName Application -Source Application -EntryType Error -EventId ' + - IntToStr(EventId) + - ' -Message (Get-Content -Raw -LiteralPath ''' + TempFile + ''') } catch { exit 1 }"'; - - if Exec( - ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'), - Cmd, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('WriteApplicationEventError: EventId=' + IntToStr(EventId) + ' exit=' + IntToStr(ResultCode)) - else - Log('WriteApplicationEventError: failed to start PowerShell for EventId=' + IntToStr(EventId)); - - DeleteFile(TempFile); -end; - -procedure FlushInstallLog(); -begin - if (InstallLogPath = '') or not InstallLogHasIssues then - Exit; - DeleteFile(InstallLogPath); - SaveStringToFile(InstallLogPath, InstallLogBuffer, False); -end; - -procedure ResetLegacyCleanupWarnings(); -begin - LegacyCleanupWarnings := ''; -end; - -procedure AppendLegacyCleanupWarning(const Message: String); -begin - if Message = '' then - Exit; - - if LegacyCleanupWarnings = '' then - LegacyCleanupWarnings := Message - else - LegacyCleanupWarnings := LegacyCleanupWarnings + #13#10 + Message; - - AppendInstallLogWarn('Legacy cleanup: ' + Message); -end; - -function HasLegacyCleanupWarnings(): Boolean; -begin - Result := Trim(LegacyCleanupWarnings) <> ''; -end; - -procedure ResetSkippedSymlinkLog(); -begin - SkippedSymlinkLogPath := ExpandConstant('{app}\symlinks.log'); - SkippedSymlinkLogBuffer := - 'NVM for Windows could not recreate the following migrated links automatically.' + #13#10 + - 'Run these commands manually after setup if you still need the links.' + #13#10 + #13#10; - SkippedSymlinkCount := 0; - if SkippedSymlinkLogPath <> '' then - DeleteFile(SkippedSymlinkLogPath); -end; - -procedure AppendSkippedSymlink(const SourcePath, CommandLine, FailureReason: String); -begin - if (SourcePath = '') and (CommandLine = '') then - Exit; - - SkippedSymlinkCount := SkippedSymlinkCount + 1; - if SourcePath <> '' then - SkippedSymlinkLogBuffer := SkippedSymlinkLogBuffer + 'Source: ' + SourcePath + #13#10; - if CommandLine <> '' then - SkippedSymlinkLogBuffer := SkippedSymlinkLogBuffer + CommandLine + #13#10; - if FailureReason <> '' then - SkippedSymlinkLogBuffer := SkippedSymlinkLogBuffer + 'Reason: ' + FailureReason + #13#10; - SkippedSymlinkLogBuffer := SkippedSymlinkLogBuffer + #13#10; - - AppendInstallLogWarn('Automatic junction recreation failed for ' + SourcePath + ': ' + FailureReason); -end; - -procedure FlushSkippedSymlinkLog(); -begin - if SkippedSymlinkLogPath = '' then - Exit; - - if SkippedSymlinkCount = 0 then - begin - DeleteFile(SkippedSymlinkLogPath); - Exit; - end; - - DeleteFile(SkippedSymlinkLogPath); - SaveStringToFile(SkippedSymlinkLogPath, SkippedSymlinkLogBuffer, False); -end; - -function HasSkippedSymlinks(): Boolean; -begin - Result := SkippedSymlinkCount > 0; -end; - -function EscapeSingleQuotedPowerShellString(const Value: String): String; -begin - Result := Value; - StringChangeEx(Result, '''', '''''', True); -end; - -function EscapeDoubleQuotedCommandArgument(const Value: String): String; -begin - Result := Value; - StringChangeEx(Result, '"', '""', True); -end; - -function BuildMklinkJunctionCommand(const LinkPath, TargetPath: String): String; -begin - Result := - 'cmd.exe /C mklink /J "' + EscapeDoubleQuotedCommandArgument(LinkPath) + - '" "' + EscapeDoubleQuotedCommandArgument(TargetPath) + '"'; -end; - -function PathIsUnderOrEqualRoot(const Candidate, Root: String): Boolean; -var - NormCandidate: String; - NormRoot: String; -begin - NormRoot := LowerCase(NormalizePath(Root)); - if NormRoot = '' then - begin - Result := True; - Exit; - end; - - NormCandidate := LowerCase(NormalizePath(Candidate)); - if CompareText(NormCandidate, NormRoot) = 0 then - begin - Result := True; - Exit; - end; - - Result := - (Length(NormCandidate) > Length(NormRoot)) and - (Copy(NormCandidate, 1, Length(NormRoot) + 1) = NormRoot + '\'); -end; - -function ExtractNodeVersionFromDirectoryPath(const PathValue: String): String; -var - Name: String; - I: Integer; -begin - Result := ''; - Name := ExtractFileName(NormalizePath(PathValue)); - if (Length(Name) > 0) and ((Name[1] = 'v') or (Name[1] = 'V')) then - Delete(Name, 1, 1); - Name := Trim(Name); - if Name = '' then - Exit; - - for I := 1 to Length(Name) do - begin - if not (((Name[I] >= '0') and (Name[I] <= '9')) or (Name[I] = '.')) then - Exit; - end; - - if (Name[1] < '0') or (Name[1] > '9') then - Exit; - - Result := Name; -end; - -{ Resolve a junction/symlink target without executing anything under that path. } -function ResolveReparseTarget(const LinkPath: String): String; -var - ScriptFile: String; - ResultFile: String; - ScriptText: String; - ScriptOutput: AnsiString; - ResultCode: Integer; - FindRec: TFindRec; - Attr: Integer; -begin - Result := ''; - - if not DirExists(LinkPath) then - Exit; - - if FindFirst(LinkPath, FindRec) then - begin - Attr := FindRec.Attributes; - FindClose(FindRec); - end - else - Attr := -1; - - if (Attr = -1) or ((Attr and FILE_ATTRIBUTE_REPARSE_POINT) = 0) then - Exit; - - ScriptFile := ExpandConstant('{tmp}\nvm-resolve-symlink.ps1'); - ResultFile := ExpandConstant('{tmp}\nvm-resolve-symlink-result.txt'); - DeleteFile(ResultFile); - - ScriptText := - '$link = ''' + EscapeSingleQuotedPowerShellString(LinkPath) + '''' + #13#10 + - '$resultFile = ''' + EscapeSingleQuotedPowerShellString(ResultFile) + '''' + #13#10 + - 'try {' + #13#10 + - ' $item = Get-Item -LiteralPath $link -Force -ErrorAction Stop' + #13#10 + - ' $target = $item.Target' + #13#10 + - ' if ($null -eq $target) { throw "Link target not available." }' + #13#10 + - ' if ($target -is [array]) { $target = $target[0] }' + #13#10 + - ' $target = [string]$target' + #13#10 + - ' if ([string]::IsNullOrWhiteSpace($target)) { throw "Link target is empty." }' + #13#10 + - ' if (-not [System.IO.Path]::IsPathRooted($target)) {' + #13#10 + - ' $target = Join-Path -Path (Split-Path -Parent $link) -ChildPath $target' + #13#10 + - ' }' + #13#10 + - ' $target = [System.IO.Path]::GetFullPath($target)' + #13#10 + - ' [System.IO.File]::WriteAllText($resultFile, $target)' + #13#10 + - ' exit 0' + #13#10 + - '} catch {' + #13#10 + - ' exit 1' + #13#10 + - '}' ; - - SaveStringToFile(ScriptFile, ScriptText, False); - if not Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - begin - DeleteFile(ScriptFile); - Exit; - end; - - if (ResultCode = 0) and LoadStringFromFile(ResultFile, ScriptOutput) then - Result := Trim(String(ScriptOutput)); - - DeleteFile(ScriptFile); - DeleteFile(ResultFile); -end; - -{ Derive the v1 active Node version from NVM_SYMLINK metadata only. } -{ Never execute legacy node.exe β€” a replaced junction must not run code. } -function GetNodeVersionFromSymlinkEnv(const SymlinkPath: String): String; -var - TargetPath: String; - TrustedRoot: String; -begin - Result := ''; - - TargetPath := ResolveReparseTarget(Trim(SymlinkPath)); - if TargetPath = '' then - Exit; - - TrustedRoot := Trim(LegacySettingsRoot); - if (TrustedRoot <> '') and (not PathIsUnderOrEqualRoot(TargetPath, TrustedRoot)) then - Exit; - - { Presence check only β€” do not execute this binary. } - if not FileExists(AddBackslash(TargetPath) + 'node.exe') then - Exit; - - Result := ExtractNodeVersionFromDirectoryPath(TargetPath); -end; - -procedure DeleteLegacySymlinkEnv(); -begin - RegDeleteValue(HKCU, 'Environment', 'NVM_SYMLINK'); - RegDeleteValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_SYMLINK'); -end; - -procedure CaptureLegacyActiveVersionFromSymlink(); -var - SymlinkPath: String; -begin - DetectedActiveVersion := ''; - SymlinkPath := Trim(GetEnv('NVM_SYMLINK')); - if SymlinkPath = '' then - Exit; - - DetectedActiveVersion := GetNodeVersionFromSymlinkEnv(SymlinkPath); - DeleteLegacySymlinkEnv(); -end; - -function GetActiveVersionRegistryValue(Param: String): String; -begin - Result := DetectedActiveVersion; - - if Result <> '' then - Exit; - - if not RegQueryStringValue(HKCU, '{#RegistryKey}', 'ActiveVersion', Result) then - Result := ''; -end; - -function GetInstallRoot(Param: String): String; -begin - Result := GetRealInstallRoot(''); - - if (NodeStoragePage <> nil) and (Trim(NodeStoragePage.Values[0]) <> '') then - Result := Trim(NodeStoragePage.Values[0]); -end; - -// DataRoot is the parent of InstallRoot (.shim / .nodejs / .cache live here). -// Default InstallRoot={app}\installs keeps DataRoot={app}. Custom storage splits them. -function GetDataRoot(Param: String): String; -var - InstallRoot: String; -begin - InstallRoot := NormalizePath(GetInstallRoot('')); - if InstallRoot = '' then - begin - Result := ExpandConstant('{app}'); - Exit; - end; - - Result := NormalizePath(ExtractFileDir(InstallRoot)); - if Result = '' then - Result := ExpandConstant('{app}'); -end; - -function GetLegacySettingValue(const SettingsText: String; const Key: String): String; -var - Normalized: String; - Line: String; - SearchKey: String; - P: Integer; - L: Integer; -begin - Result := ''; - Normalized := SettingsText; - SearchKey := LowerCase(Key) + ':'; - - while Length(Normalized) > 0 do - begin - P := Pos(#10, Normalized); - if P > 0 then - begin - Line := Trim(Copy(Normalized, 1, P - 1)); - Delete(Normalized, 1, P); - end - else - begin - Line := Trim(Normalized); - Normalized := ''; - end; - - L := Length(SearchKey); - if (Length(Line) >= L) and (Copy(LowerCase(Line), 1, L) = SearchKey) then - begin - Result := Trim(Copy(Line, L + 1, Length(Line))); - Exit; - end; - end; -end; - -procedure LoadLegacySettings(); -var - SettingsText: AnsiString; - SettingsFile: String; -begin - LegacySettingsLoaded := False; - LegacySettingsRoot := ''; - LegacySettingsPath := ''; - LegacySettingsNodeMirror := ''; - LegacySettingsNpmMirror := ''; - LegacyNodeMirrorFound := False; - LegacyNpmMirrorFound := False; - - if LegacyInstallDir = '' then - Exit; - - SettingsFile := AddBackslash(LegacyInstallDir) + 'settings.txt'; - if not LoadStringFromFile(SettingsFile, SettingsText) then - Exit; - - LegacySettingsLoaded := True; - LegacySettingsRoot := GetLegacySettingValue(String(SettingsText), 'root'); - LegacySettingsPath := GetLegacySettingValue(String(SettingsText), 'path'); - LegacySettingsNodeMirror := GetLegacySettingValue(String(SettingsText), 'node_mirror'); - LegacySettingsNpmMirror := GetLegacySettingValue(String(SettingsText), 'npm_mirror'); - LegacyNodeMirrorFound := Trim(LegacySettingsNodeMirror) <> ''; - LegacyNpmMirrorFound := Trim(LegacySettingsNpmMirror) <> ''; -end; - -function ParseSettingsTxtRoot(const NvmDir: String): String; -var - FilePath: String; - Content: AnsiString; - ContentStr: String; - LineStart, LineEnd: Integer; - Line: String; - ColonPos: Integer; -begin - Result := ''; - FilePath := AddBackslash(NvmDir) + 'settings.txt'; - if not FileExists(FilePath) then - Exit; - if not LoadStringFromFile(FilePath, Content) then - Exit; - ContentStr := String(Content); - LineStart := 1; - while LineStart <= Length(ContentStr) do - begin - LineEnd := LineStart; - while (LineEnd <= Length(ContentStr)) and - (ContentStr[LineEnd] <> #13) and - (ContentStr[LineEnd] <> #10) do - LineEnd := LineEnd + 1; - Line := Trim(Copy(ContentStr, LineStart, LineEnd - LineStart)); - LineStart := LineEnd + 1; - if (LineStart <= Length(ContentStr)) and - (ContentStr[LineStart - 1] = #13) and - (ContentStr[LineStart] = #10) then - LineStart := LineStart + 1; - ColonPos := Pos(':', Line); - if ColonPos > 0 then - begin - if CompareText(Trim(Copy(Line, 1, ColonPos - 1)), 'root') = 0 then - begin - Result := Trim(Copy(Line, ColonPos + 1, Length(Line))); - Exit; - end; - end; - end; -end; - -function GetLegacyV1NodeStorageRoot(): String; -begin - Result := Trim(LegacySettingsRoot); - if Result = '' then - Result := GetRealInstallRoot(''); -end; - -function GetLegacyV1InstalledVersion(): String; -begin - Result := ''; - if not RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\nvm_is1', 'DisplayVersion', Result) then - Result := ''; - Result := Trim(Result); -end; - -function ConvertCsvToMultiSz(const Value: String): String; -var - Remaining: String; - Part: String; - P: Integer; -begin - Result := ''; - Remaining := Trim(Value); - if Remaining = '' then - Exit; - - P := Pos(',', Remaining); - while P > 0 do - begin - Part := Trim(Copy(Remaining, 1, P - 1)); - if Part <> '' then - begin - if Result = '' then - Result := Part - else - Result := Result + #0 + Part; - end; - Delete(Remaining, 1, P); - P := Pos(',', Remaining); - end; - - Part := Trim(Remaining); - if Part <> '' then - begin - if Result = '' then - Result := Part - else - Result := Result + #0 + Part; - end; -end; - -function GetNodeMirrorRegistryValue(Param: String): String; -begin - if IsPreV2Upgrade then - begin - if LegacyNodeMirrorFound then - Result := ConvertCsvToMultiSz(LegacySettingsNodeMirror) - else - Result := ''; - Exit; - end; - - if LegacySettingsLoaded and LegacyNodeMirrorFound then - Result := ConvertCsvToMultiSz(LegacySettingsNodeMirror) - else - Result := 'https://nodejs.org/dist'; -end; - -function GetNpmMirrorRegistryValue(Param: String): String; -begin - if IsPreV2Upgrade then - begin - if LegacyNpmMirrorFound then - Result := ConvertCsvToMultiSz(LegacySettingsNpmMirror) - else - Result := ''; - Exit; - end; - - if LegacySettingsLoaded and LegacyNpmMirrorFound then - Result := ConvertCsvToMultiSz(LegacySettingsNpmMirror) - else - Result := 'https://registry.npmjs.org'; -end; - -function ShouldWriteNodeMirror(): Boolean; -begin - if IsPreV2Upgrade then - Result := LegacyNodeMirrorFound - else - Result := True; -end; - -function ShouldWriteNpmMirror(): Boolean; -begin - if IsPreV2Upgrade then - Result := LegacyNpmMirrorFound - else - Result := True; -end; - -procedure RemoveLegacyScheduledTasks(); -var - TaskCleanupScript: String; - TaskCleanupScriptFile: String; - ResultCode: Integer; -begin - TaskCleanupScriptFile := ExpandConstant('{tmp}\nvm-legacy-task-cleanup.ps1'); - TaskCleanupScript := - '$knownNames = @(''NVM Sync'', ''NVM for Windows Sync'')' + #13#10 + - 'Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {' + #13#10 + - ' $isKnownName = $knownNames -contains $_.TaskName' + #13#10 + - ' $isNvmSyncAction = $false' + #13#10 + - ' foreach ($a in $_.Actions) {' + #13#10 + - ' $exeFull = (($a.Execute | Out-String).Trim()).ToLowerInvariant()' + #13#10 + - ' $exeFull = $exeFull -replace ''/'', ''\''' + #13#10 + - ' $exe = [System.IO.Path]::GetFileName($exeFull)' + #13#10 + - ' $args = (($a.Arguments | Out-String).Trim()).ToLowerInvariant()' + #13#10 + - ' if (($exe -eq ''nvm.exe'') -and ($args -match ''(^|\s)sync(\s|$)'')) { $isNvmSyncAction = $true; break }' + #13#10 + - ' }' + #13#10 + - ' $isKnownName -or $isNvmSyncAction' + #13#10 + - '} | ForEach-Object {' + #13#10 + - ' Unregister-ScheduledTask -TaskName $_.TaskName -TaskPath $_.TaskPath -Confirm:$false -ErrorAction SilentlyContinue' + #13#10 + - '}'; - - SaveStringToFile(TaskCleanupScriptFile, TaskCleanupScript, False); - ExecStep( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + TaskCleanupScriptFile + '"', - 'NVM4102', - 'Removing scheduled tasks exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ); - DeleteFile(TaskCleanupScriptFile); -end; - -procedure CreateSyncScheduledTask(); -var - TaskCreateCommand: String; - TaskAuthorScript: String; - TaskAuthorScriptFile: String; - QueryOutputFile: String; - QueryOutputRaw: AnsiString; - SyncExePath: String; - ResultCode: Integer; -begin - SyncExePath := ExpandConstant('{app}\utils\sync.exe'); - if not FileExists(SyncExePath) then - begin - Log('Sync executable not found at "' + SyncExePath + '"; skipping sync task setup.'); - Exit; - end; - - TaskCreateCommand := - '/C schtasks /Create /SC HOURLY /MO 1 /TN "' + SyncTaskName + - '" /TR "\"' + SyncExePath + '\" --background sync" /F'; - - if not ExecStep( - ExpandConstant('{cmd}'), - TaskCreateCommand, - 'NVM4108', - 'Creating the sync scheduled task exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ) then - begin - Log('Unable to create sync scheduled task.'); - Exit; - end; - - if ResultCode <> 0 then - Log('Sync scheduled task creation returned exit code ' + IntToStr(ResultCode) + '.'); - - { Ensure task metadata shows the product author, while keeping task principal as current user } - TaskAuthorScriptFile := ExpandConstant('{tmp}\nvm-sync-task-author.ps1'); - TaskAuthorScript := - '$taskName = ''' + SyncTaskName + '''' + #13#10 + - '$taskAuthor = ''{#OrgLabel}''' + #13#10 + - '$service = New-Object -ComObject ''Schedule.Service''' + #13#10 + - '$service.Connect()' + #13#10 + - '$folder = $service.GetFolder(''\'')' + #13#10 + - '$task = $folder.GetTask($taskName)' + #13#10 + - 'if ($null -eq $task) { exit 2 }' + #13#10 + - '$definition = $task.Definition' + #13#10 + - '$definition.RegistrationInfo.Author = $taskAuthor' + #13#10 + - '$folder.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, $definition.Principal.LogonType, $null) | Out-Null'; - - SaveStringToFile(TaskAuthorScriptFile, TaskAuthorScript, False); - if not ExecStep( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + TaskAuthorScriptFile + '"', - 'NVM4108', - 'Creating the sync scheduled task exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ) then - begin - Log('Unable to update sync task author metadata.'); - end - else if ResultCode <> 0 then - begin - Log('Sync task author metadata update returned exit code ' + IntToStr(ResultCode) + '.'); - end; - DeleteFile(TaskAuthorScriptFile); - - QueryOutputFile := ExpandConstant('{tmp}') + '\nvm-sync-task-query.txt'; - DeleteFile(QueryOutputFile); - - if not ExecStep( - ExpandConstant('{cmd}'), - '/C schtasks /Query /TN "' + SyncTaskName + '" /FO LIST /V > "' + QueryOutputFile + '" 2>&1', - 'NVM4108', - 'Creating the sync scheduled task exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ) then - begin - Log('Unable to verify sync scheduled task registration (query command failed to start).'); - end - else if ResultCode <> 0 then - begin - Log('Unable to verify sync scheduled task registration (query exit code ' + IntToStr(ResultCode) + ').'); - end - else if LoadStringFromFile(QueryOutputFile, QueryOutputRaw) then - begin - Log('Sync scheduled task verified by schtasks query.'); - Log('Sync task query output: ' + Trim(String(QueryOutputRaw))); - end - else - begin - Log('Sync scheduled task query completed, but output could not be read.'); - end; - - DeleteFile(QueryOutputFile); -end; - -function IsSafeRemovableDirectory(const PathValue: String): Boolean; -var - Normalized: String; -begin - Normalized := Trim(PathValue); - StringChangeEx(Normalized, '/', '\\', True); - while (Length(Normalized) > 0) and (Normalized[Length(Normalized)] = '\\') do - Delete(Normalized, Length(Normalized), 1); - - Result := (Length(Normalized) > 3) and - not ((Length(Normalized) = 2) and (Normalized[2] = ':')); -end; - -procedure RemoveLegacyFilesFromSameDirectory(const RootDir: String); -var - CleanupScriptFile: String; - CleanupScript: String; - RootEscaped: String; - ResultCode: Integer; -begin - RootEscaped := RootDir; - StringChangeEx(RootEscaped, '''', '''''', True); - - CleanupScriptFile := ExpandConstant('{tmp}\\nvm-legacy-file-cleanup.ps1'); - CleanupScript := - '$root = ''' + RootEscaped + '''' + #13#10 + - '$keep = @(''nvm.exe'',''NVMWindows.Events.man'',''NVMWindows.Events.dll'',''utils'',''.shim'',''.sync'',''.icons'')' + #13#10 + - 'if (Test-Path -LiteralPath $root) {' + #13#10 + - ' Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue | ForEach-Object {' + #13#10 + - ' $n = $_.Name' + #13#10 + - ' if ($keep -contains $n) { return }' + #13#10 + - ' if ($n -like ''unins*.exe'' -or $n -like ''unins*.dat'' -or $n -like ''unins*.msg'') { return }' + #13#10 + - ' Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue' + #13#10 + - ' }' + #13#10 + - '}'; - - SaveStringToFile(CleanupScriptFile, CleanupScript, False); - Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + CleanupScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ); - DeleteFile(CleanupScriptFile); -end; - -procedure CleanupLegacyInstallLocationIfNeeded(); -var - LegacyRoot: String; - CurrentAppRoot: String; - PreviousRoot: String; -begin - if not IsPreV2Upgrade then - Exit; - - LegacyRoot := Trim(LegacyInstallDir); - if LegacyRoot = '' then - Exit; - - CurrentAppRoot := ExpandConstant('{app}'); - - if SameText(RemoveBackslashUnlessRoot(Trim(LegacyRoot)), RemoveBackslashUnlessRoot(Trim(CurrentAppRoot))) then - begin - RemoveLegacyFilesFromSameDirectory(CurrentAppRoot); - Exit; - end; - - PreviousRoot := Trim(PreviousInstallRoot); - if (PreviousRoot <> '') and - SameText(RemoveBackslashUnlessRoot(Trim(LegacyRoot)), RemoveBackslashUnlessRoot(Trim(PreviousRoot))) and - (not MigrationPerformed) then - Exit; - - if IsSafeRemovableDirectory(LegacyRoot) and DirExists(LegacyRoot) then - DelTree(LegacyRoot, True, True, True); -end; - -function IsNvmDisplayName(const DisplayName: String): Boolean; -var - L: String; -begin - L := LowerCase(Trim(DisplayName)); - Result := (Pos('nvm for windows', L) > 0) or (Pos('node version manager for windows', L) > 0); -end; - -procedure RemoveLegacyWindowsAppsEntries(); -var - SubKeys: TArrayOfString; - I: Integer; - KeyName: String; - DisplayName: String; -begin - if not RegGetSubkeyNames(HKCU, WindowsAppsUninstallRoot, SubKeys) then - Exit; - - for I := 0 to GetArrayLength(SubKeys) - 1 do - begin - KeyName := SubKeys[I]; - if CompareText(KeyName, '{#AppId}_is1') = 0 then - Continue; - - DisplayName := ''; - if RegQueryStringValue(HKCU, WindowsAppsUninstallRoot + '\\' + KeyName, 'DisplayName', DisplayName) and - IsNvmDisplayName(DisplayName) then - RegDeleteKeyIncludingSubkeys(HKCU, WindowsAppsUninstallRoot + '\\' + KeyName); - end; -end; - -function IsSameExpandedPath(const Segment, OtherPath: String): Boolean; -var - ExpandedSeg: String; -begin - ExpandedSeg := NormalizePath(ExpandPathSegment(Segment)); - Result := (ExpandedSeg <> '') and (CompareText(ExpandedSeg, NormalizePath(OtherPath)) = 0); -end; - -function ContainsNormalizedPath(const SeenPaths: TArrayOfString; const Value: String): Boolean; -var - I: Integer; -begin - Result := False; - if Value = '' then - Exit; - - for I := 0 to GetArrayLength(SeenPaths) - 1 do - begin - if CompareText(SeenPaths[I], Value) = 0 then - begin - Result := True; - Exit; - end; - end; -end; - -procedure AddNormalizedPath(var SeenPaths: TArrayOfString; const Value: String); -var - N: Integer; -begin - if Value = '' then - Exit; - - N := GetArrayLength(SeenPaths); - SetArrayLength(SeenPaths, N + 1); - SeenPaths[N] := Value; -end; - -function BuildCleanedPath( - const OriginalPath: String; - const RemoveLegacyEntries: Boolean; - const ShouldPrependCurrentNvm: Boolean -): String; -var - Segments: TArrayOfString; - Seen: TArrayOfString; - NewPath: String; - Segment: String; - NormalizedExpanded: String; - NormalizedLegacyHome: String; - NormalizedLegacyNodePath: String; - NormalizedCurrentHome: String; - NormalizedCurrentNodePath: String; - NormalizedProgramNodePath: String; - I: Integer; - RemoveLegacyHome: Boolean; - RemoveLegacyNodePath: Boolean; -begin - Result := OriginalPath; - NewPath := ''; - SetArrayLength(Seen, 0); - - SplitPathString(OriginalPath, Segments); - - NormalizedLegacyHome := NormalizePath(Trim(LegacyInstallDir)); - NormalizedLegacyNodePath := NormalizePath(Trim(LegacySettingsPath)); - NormalizedCurrentHome := NormalizePath(ExpandConstant('{app}')); - // PATH node entry must track DataRoot (.shim lives there), not always {app}. - NormalizedCurrentNodePath := NormalizePath(AddBackslash(GetDataRoot('')) + '.nodejs'); - NormalizedProgramNodePath := NormalizePath(ExpandConstant('{app}\\.nodejs')); - - RemoveLegacyHome := RemoveLegacyEntries and (NormalizedLegacyHome <> '') and - (CompareText(NormalizedLegacyHome, NormalizedCurrentHome) <> 0); - RemoveLegacyNodePath := RemoveLegacyEntries and (NormalizedLegacyNodePath <> '') and - (CompareText(NormalizedLegacyNodePath, NormalizedCurrentNodePath) <> 0); - - for I := 0 to GetArrayLength(Segments) - 1 do - begin - Segment := Segments[I]; - if Segment = '' then - Continue; - - if CompareText(Segment, '%NVM_SYMLINK%') = 0 then - Continue; - - if ShouldPrependCurrentNvm then - begin - if (CompareText(Segment, '%NVM_HOME%') = 0) or - (CompareText(Segment, '%NVM_HOME%\\.nodejs') = 0) or - IsSameExpandedPath(Segment, NormalizedCurrentHome) or - IsSameExpandedPath(Segment, NormalizedCurrentNodePath) or - IsSameExpandedPath(Segment, NormalizedProgramNodePath) then - Continue; - end; - - if RemoveLegacyHome and IsSameExpandedPath(Segment, NormalizedLegacyHome) then - Continue; - - if RemoveLegacyNodePath and IsSameExpandedPath(Segment, NormalizedLegacyNodePath) then - Continue; - - NormalizedExpanded := NormalizePath(ExpandPathSegment(Segment)); - if (NormalizedExpanded <> '') and ContainsNormalizedPath(Seen, NormalizedExpanded) then - Continue; - - if NormalizedExpanded <> '' then - AddNormalizedPath(Seen, NormalizedExpanded); - - if NewPath = '' then - NewPath := Segment - else - NewPath := NewPath + ';' + Segment; - end; - - if ShouldPrependCurrentNvm then - begin - if NewPath = '' then - NewPath := NormalizedCurrentHome + ';' + NormalizedCurrentNodePath - else - NewPath := NormalizedCurrentHome + ';' + NormalizedCurrentNodePath + ';' + NewPath; - end; - - Result := NewPath; -end; - -procedure RemoveLegacySystemPathEntries(); -var - SystemPath: String; - NewSystemPath: String; -begin - if not IsPreV2Upgrade then - Exit; - - if not RegQueryStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', SystemPath) then - Exit; - - NewSystemPath := BuildCleanedPath(SystemPath, True, False); - if CompareText(NewSystemPath, SystemPath) = 0 then - Exit; - - if not RegWriteExpandStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', NewSystemPath) then - AppendLegacyCleanupWarning('Could not update the System PATH to remove legacy NVM entries (permissions may be required).'); -end; - -function IsLegacyUninstallEntry(const Root: Integer; const SubKey: String; const KeyName: String): Boolean; -var - DisplayName: String; - UninstallString: String; - DisplayIcon: String; - NormalizedLegacyInstallDir: String; - LowerUninstall: String; - LowerIcon: String; -begin - Result := False; - - if CompareText(KeyName, '{#AppId}_is1') = 0 then - Exit; - - if CompareText(KeyName, 'nvm_is1') = 0 then - begin - Result := True; - Exit; - end; - - DisplayName := ''; - RegQueryStringValue(Root, SubKey, 'DisplayName', DisplayName); - if IsNvmDisplayName(DisplayName) then - begin - Result := True; - Exit; - end; - - NormalizedLegacyInstallDir := LowerCase(NormalizePath(Trim(LegacyInstallDir))); - if NormalizedLegacyInstallDir = '' then - Exit; - - UninstallString := ''; - DisplayIcon := ''; - RegQueryStringValue(Root, SubKey, 'UninstallString', UninstallString); - RegQueryStringValue(Root, SubKey, 'DisplayIcon', DisplayIcon); - - LowerUninstall := LowerCase(UninstallString); - LowerIcon := LowerCase(DisplayIcon); - - StringChangeEx(LowerUninstall, '/', '\\', True); - StringChangeEx(LowerIcon, '/', '\\', True); - - if (Pos(NormalizedLegacyInstallDir, LowerUninstall) > 0) or - (Pos(NormalizedLegacyInstallDir, LowerIcon) > 0) then - Result := True; -end; - -procedure RemoveLegacyUninstallEntriesFromRoot(const Root: Integer; const UninstallRoot: String; const RootLabel: String); -var - SubKeys: TArrayOfString; - I: Integer; - KeyName: String; - FullSubKey: String; -begin - if not RegGetSubkeyNames(Root, UninstallRoot, SubKeys) then - Exit; - - for I := 0 to GetArrayLength(SubKeys) - 1 do - begin - KeyName := SubKeys[I]; - FullSubKey := UninstallRoot + '\\' + KeyName; - - if not IsLegacyUninstallEntry(Root, FullSubKey, KeyName) then - Continue; - - if not RegDeleteKeyIncludingSubkeys(Root, FullSubKey) then - AppendLegacyCleanupWarning( - 'Could not remove legacy uninstall entry "' + KeyName + '" from ' + RootLabel + - ' (permissions may be required).' - ); - end; -end; - -procedure RemoveLegacyV1UninstallEntries(); -begin - if not IsPreV2Upgrade then - Exit; - - RemoveLegacyUninstallEntriesFromRoot(HKCU, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall', 'HKCU'); - RemoveLegacyUninstallEntriesFromRoot(HKLM, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall', 'HKLM'); - RemoveLegacyUninstallEntriesFromRoot(HKLM, 'Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall', 'HKLM\\WOW6432Node'); -end; - -procedure RemoveLegacySymlinkPathIfNeeded(); -var - LegacySymlinkPath: String; - NewNodePath: String; - Attr: Integer; - ResultCode: Integer; - FindRec: TFindRec; - ElevatedExecOk: Boolean; -begin - LegacySymlinkPath := NormalizePath(Trim(LegacySettingsPath)); - if LegacySymlinkPath = '' then - LegacySymlinkPath := NormalizePath(Trim(GetEnv('NVM_SYMLINK'))); - if LegacySymlinkPath = '' then - Exit; - - NewNodePath := NormalizePath(AddBackslash(GetDataRoot('')) + '.nodejs'); - if CompareText(LegacySymlinkPath, NewNodePath) = 0 then - Exit; - - if not DirExists(LegacySymlinkPath) then - Exit; - - if FindFirst(LegacySymlinkPath, FindRec) then - begin - Attr := FindRec.Attributes; - FindClose(FindRec); - end - else - Attr := -1; - - if (Attr = -1) or ((Attr and FILE_ATTRIBUTE_REPARSE_POINT) = 0) then - begin - AppendInstallLog('Legacy symlink path exists but is not a reparse point, skipping removal: ' + LegacySymlinkPath); - Exit; - end; - - if not ExecStep( - ExpandConstant('{cmd}'), - '/C rmdir "' + LegacySymlinkPath + '"', - 'NVM4104', - 'Removing the legacy Node.js link exceeded 5 minutes.', - 300, - True, - False, - ResultCode - ) then - ResultCode := 1; - - if InstallerHardFailed then - Exit; - - if ResultCode <> 0 then - begin - ElevatedExecOk := ExecStep( - ExpandConstant('{cmd}'), - '/C rmdir "' + LegacySymlinkPath + '"', - 'NVM4104', - 'Removing the legacy Node.js link exceeded 5 minutes.', - 300, - True, - True, - ResultCode - ); - if (not ElevatedExecOk) or (ResultCode <> 0) then - AppendLegacyCleanupWarning('Could not remove the legacy symlink path: ' + LegacySymlinkPath + ' (exit code ' + IntToStr(ResultCode) + ').') - else - AppendInstallLog('Removed legacy symlink path: ' + LegacySymlinkPath); - end - else - AppendInstallLog('Removed legacy symlink path: ' + LegacySymlinkPath); -end; - -{ ── Elevated migration helper ────────────────────────────────────────────── } - -{ Run nvm-upgrader.exe (elevated) to perform all privileged v1-cleanup tasks } -{ in a single UAC prompt: remove NVM_SYMLINK/NVM_HOME env vars from user and } -{ system environments, strip v1 PATH entries from both, delete v1 uninstall } -{ registry keys, remove the old install directory, and register the Windows } -{ Event Log source in the new nvm.exe. } -procedure RunUpgraderHelper(); -var - UpgraderArgs: String; - ResultCode: Integer; - ElevatedExecOk: Boolean; -begin - UpgraderArgs := ''; - - { Pass --install-dir only when the v1 location differs from the new install directory. } - if (LegacyInstallDir <> '') and - not SameText( - RemoveBackslashUnlessRoot(Trim(LegacyInstallDir)), - RemoveBackslashUnlessRoot(ExpandConstant('{app}')) - ) then - UpgraderArgs := UpgraderArgs + ' --install-dir "' + LegacyInstallDir + '"'; - - { Pass --symlink-path only when it differs from the new .nodejs junction. } - if (LegacySettingsPath <> '') and - not SameText( - RemoveBackslashUnlessRoot(Trim(LegacySettingsPath)), - RemoveBackslashUnlessRoot(AddBackslash(GetDataRoot('')) + '.nodejs') - ) then - UpgraderArgs := UpgraderArgs + ' --symlink-path "' + LegacySettingsPath + '"'; - - UpgraderArgs := UpgraderArgs + ' --new-nvm "' + ExpandConstant('{app}\{#Alias}.exe') + '"'; - UpgraderArgs := Trim(UpgraderArgs); - - AppendInstallLog('RunUpgraderHelper: args=' + UpgraderArgs); - - ElevatedExecOk := ShellExec( - 'runas', - ExpandConstant('{tmp}\nvm-upgrader.exe'), - UpgraderArgs, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ); - - if not ElevatedExecOk then - begin - AppendInstallLogWarn('RunUpgraderHelper: ShellExec failed, code=' + IntToStr(ResultCode)); - if ResultCode = 1223 then - MsgBox( - 'NVM for Windows could not run the v1 migration cleanup helper because the UAC prompt was canceled.' + #13#10 + #13#10 + - 'Your previous NVM v1 installation may leave behind environment variables (NVM_HOME, NVM_SYMLINK)' + #13#10 + - 'and PATH entries that could conflict with NVM for Windows 2.' + #13#10 + #13#10 + - 'To clean up manually: Start > "Edit the system environment variables" > Environment Variables' + #13#10 + - '> delete NVM_HOME and NVM_SYMLINK from System variables, and remove any references to' + #13#10 + - 'those variables from the System PATH.' + #13#10 + #13#10 + - 'Event Log registration can be completed later from an elevated terminal:' + #13#10 + - ExpandConstant('{app}\{#Alias}.exe') + ' --register-eventlog', - mbInformation, - MB_OK - ) - else - AppendInstallLogWarn( - 'RunUpgraderHelper: elevation failed with system error ' + IntToStr(ResultCode) - ); - end - else if ResultCode <> 0 then - begin - AppendInstallLogWarn('RunUpgraderHelper: helper exited with code ' + IntToStr(ResultCode)); - MsgBox( - 'The NVM v1 migration cleanup helper exited with an unexpected code (' + IntToStr(ResultCode) + ').' + #13#10 + #13#10 + - 'Some v1 leftovers (environment variables, PATH entries, or the old install folder) may not have been removed.' + #13#10 + #13#10 + - 'Check the log for details:' + #13#10 + - ExpandConstant('{app}\upgrade.log'), - mbInformation, - MB_OK - ); - end; -end; - -function GetOperatingModeRegistryValue(Param: String): String; -begin - Result := 'shim'; - if (OperatingModePage <> nil) and OperatingModePage.Values[1] then - Result := 'link'; -end; - -function IsShimModeSelected(): Boolean; -begin - Result := CompareText(GetOperatingModeRegistryValue(''), 'shim') = 0; -end; - -function GetCacheDownloadsRegistryValue(Param: String): String; -begin - Result := '0'; - if (PreferencesPage <> nil) and PreferencesPage.Values[0] then - Result := '1'; -end; - -function GetAutoUseRegistryValue(Param: String): String; -begin - Result := '1'; - if (PreferencesPage <> nil) and not PreferencesPage.Values[1] then - Result := '0'; -end; - -function IsExistingInstallDetected(): Boolean; -begin - // Only treat the current v2 uninstall entry as proof of an installed copy. - // Preferences can survive an uninstall failure and should only be reused as - // wizard defaults, not as a reinstall/upgrade signal. - Result := RegValueExists( - HKCU, - 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#AppId}_is1', - 'DisplayVersion' - ); -end; - -function GetExistingInstallVersion(): String; -var - InstalledVersion: String; -begin - Result := 'unknown version'; - - if RegQueryStringValue( - HKCU, - 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#AppId}_is1', - 'DisplayVersion', - InstalledVersion - ) and (Trim(InstalledVersion) <> '') then - begin - Result := Trim(InstalledVersion); - Exit; - end; - - if RegQueryStringValue(HKCU, '{#RegistryKey}', 'Version', InstalledVersion) and - (Trim(InstalledVersion) <> '') then - Result := Trim(InstalledVersion); -end; - -function GetExistingInstallRoot(var InstallRoot: String): Boolean; -begin - Result := RegQueryStringValue(HKCU, '{#RegistryKey}', 'InstallRoot', InstallRoot) and - (Trim(InstallRoot) <> ''); - if Result then - InstallRoot := Trim(InstallRoot); -end; - -function ConfirmExistingInstallMigration(const ExistingVersion: String): Boolean; -var - Prompt: String; -begin - Prompt := - 'An existing NVM for Windows installation was detected.' + #13#10#13#10; - - if Trim(ExistingVersion) <> '' then - Prompt := Prompt + 'Current installed version: ' + ExistingVersion + #13#10#13#10; - - Prompt := Prompt + - 'Your prior settings and Node.js versions will be migrated.' + #13#10#13#10 + - 'Do you want to continue?'; - - { SuppressibleMsgBox honors /SUPPRESSMSGBOXES and silent installs; default Yes. } - { Interactive installs still default to No via MB_DEFBUTTON2. } - Result := SuppressibleMsgBox( - Prompt, - mbConfirmation, - MB_YESNO or MB_DEFBUTTON2, - IDYES - ) = IDYES; -end; - -function ReadRegistryDwordBool(const ValueName: String; const DefaultValue: Boolean): Boolean; -var - Value: Cardinal; -begin - Result := DefaultValue; - if RegQueryDWordValue(HKCU, '{#RegistryKey}', ValueName, Value) then - Result := Value <> 0; -end; - -procedure ResetWizardDefaults(); -begin - WizardDefaultInstallRoot := GetRealInstallRoot(''); - WizardDefaultUseLinkMode := False; - WizardDefaultCacheDownloads := False; - WizardDefaultAutoDetect := True; - WizardDefaultAutoInstall := False; - WizardDefaultAutoInstallPrompt := False; - WizardDefaultRequireTls := True; -end; - -procedure LoadExistingV2WizardDefaults(); -var - ExistingInstallRoot: String; - OperatingMode: String; -begin - if GetExistingInstallRoot(ExistingInstallRoot) then - WizardDefaultInstallRoot := ExistingInstallRoot; - - if RegQueryStringValue(HKCU, '{#RegistryKey}', 'OperatingMode', OperatingMode) then - WizardDefaultUseLinkMode := CompareText(Trim(OperatingMode), 'link') = 0; - - WizardDefaultCacheDownloads := ReadRegistryDwordBool('CacheDownloads', WizardDefaultCacheDownloads); - WizardDefaultAutoDetect := ReadRegistryDwordBool('AutoUse', WizardDefaultAutoDetect); - WizardDefaultAutoInstall := ReadRegistryDwordBool('AutoInstall', WizardDefaultAutoInstall); - WizardDefaultAutoInstallPrompt := ReadRegistryDwordBool('AutoInstallPrompt', WizardDefaultAutoInstallPrompt); - WizardDefaultRequireTls := not ReadRegistryDwordBool('AllowInsecureDownloads', not WizardDefaultRequireTls); - - if not WizardDefaultAutoInstall then - WizardDefaultAutoInstallPrompt := False; -end; - -function HasExistingV2Preferences(): Boolean; -begin - Result := - RegValueExists(HKCU, '{#RegistryKey}', 'InstallRoot') or - RegValueExists(HKCU, '{#RegistryKey}', 'OperatingMode') or - RegValueExists(HKCU, '{#RegistryKey}', 'CacheDownloads') or - RegValueExists(HKCU, '{#RegistryKey}', 'AutoUse') or - RegValueExists(HKCU, '{#RegistryKey}', 'AutoInstall') or - RegValueExists(HKCU, '{#RegistryKey}', 'AutoInstallPrompt') or - RegValueExists(HKCU, '{#RegistryKey}', 'AllowInsecureDownloads'); -end; - -function NormalizePath(const PathValue: String): String; -begin - Result := RemoveBackslashUnlessRoot(Trim(PathValue)); -end; - -function IsReparsePoint(const Attributes: Integer): Boolean; -begin - Result := (Attributes and FILE_ATTRIBUTE_REPARSE_POINT) <> 0; -end; - -procedure CountFilesInTree(const RootDir: String; var FileCount: Integer); -var - FindRec: TFindRec; - CurrentPath: String; -begin - if not FindFirst(AddBackslash(RootDir) + '*', FindRec) then - Exit; - - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') then - begin - CurrentPath := AddBackslash(RootDir) + FindRec.Name; - if IsReparsePoint(FindRec.Attributes) then - AppendInstallLog('CountFilesInTree: skipping reparse point ' + NormalizePath(CurrentPath)) - else if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then - CountFilesInTree(CurrentPath, FileCount) - else - FileCount := FileCount + 1; - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -function IsLegacyVersionDirectoryName(const DirectoryName: String): Boolean; -begin - Result := - (Length(DirectoryName) > 0) and - ((DirectoryName[1] = 'v') or (DirectoryName[1] = 'V')); -end; - -function IsInstalledNodeVersionDirectory(const RootDir, DirectoryName: String): Boolean; -begin - Result := - IsLegacyVersionDirectoryName(DirectoryName) and - FileExists(AddBackslash(AddBackslash(RootDir) + DirectoryName) + 'node.exe'); -end; - -function CountInstalledNodeVersions(const InstallRoot: String): Integer; -var - FindRec: TFindRec; - RootDir: String; -begin - Result := 0; - RootDir := Trim(InstallRoot); - if (RootDir = '') or (not DirExists(RootDir)) then - Exit; - - if not FindFirst(AddBackslash(RootDir) + '*', FindRec) then - Exit; - - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') and - ((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) and - IsInstalledNodeVersionDirectory(RootDir, FindRec.Name) then - Result := Result + 1; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -function ConfirmUninstallRemoveNodeVersions(const InstallRoot: String): Boolean; -var - VersionCount: Integer; - MessageText: String; - RootDisplay: String; -begin - RootDisplay := Trim(InstallRoot); - if RootDisplay = '' then - RootDisplay := GetInstallRootForUninstall(''); - - VersionCount := CountInstalledNodeVersions(RootDisplay); - - if VersionCount = 0 then - begin - Result := True; - Exit; - end; - - MessageText := - 'Uninstalling NVM for Windows will permanently remove NVM and all Node.js versions it manages.' + #13#10 + #13#10 + - IntToStr(VersionCount) + ' installed Node.js version(s) will be deleted from:' + #13#10 + - RootDisplay + #13#10 + #13#10 + - 'This cannot be undone. Do you want to continue?'; - - Result := MsgBox(MessageText, mbConfirmation, MB_YESNO) = IDYES; -end; - -procedure CountFilesInLegacyVersionDirectories( - const RootDir: String; - var FileCount: Integer; - var DirCount: Integer -); -var - FindRec: TFindRec; - CurrentPath: String; -begin - if not FindFirst(AddBackslash(RootDir) + '*', FindRec) then - Exit; - - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') and - ((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) and - IsLegacyVersionDirectoryName(FindRec.Name) then - begin - CurrentPath := AddBackslash(RootDir) + FindRec.Name; - DirCount := DirCount + 1; - CountFilesInTree(CurrentPath, FileCount); - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -function TryMigrateReparsePointAsJunction( - const SourcePath, DestPath, SourceRoot, DestRoot: String -): Boolean; -var - ScriptFile: String; - ResultFile: String; - ScriptText: String; - ScriptOutput: AnsiString; - OutputText: String; - FailureReason: String; - CommandLine: String; - ResultCode: Integer; -begin - Result := False; - ScriptFile := ExpandConstant('{tmp}\nvm-migrate-junction.ps1'); - ResultFile := ExpandConstant('{tmp}\nvm-migrate-junction-result.txt'); - DeleteFile(ResultFile); - - ScriptText := - '$source = ''' + EscapeSingleQuotedPowerShellString(SourcePath) + '''' + #13#10 + - '$dest = ''' + EscapeSingleQuotedPowerShellString(DestPath) + '''' + #13#10 + - '$sourceRoot = ''' + EscapeSingleQuotedPowerShellString(SourceRoot) + '''' + #13#10 + - '$destRoot = ''' + EscapeSingleQuotedPowerShellString(DestRoot) + '''' + #13#10 + - '$resultFile = ''' + EscapeSingleQuotedPowerShellString(ResultFile) + '''' + #13#10 + - '$command = ""' + #13#10 + - 'try {' + #13#10 + - ' $item = Get-Item -LiteralPath $source -Force -ErrorAction Stop' + #13#10 + - ' $target = $item.Target' + #13#10 + - ' if ($null -eq $target) { throw "Link target not available." }' + #13#10 + - ' if ($target -is [array]) { $target = $target[0] }' + #13#10 + - ' $target = [string]$target' + #13#10 + - ' if ([string]::IsNullOrWhiteSpace($target)) { throw "Link target is empty." }' + #13#10 + - ' if (-not [System.IO.Path]::IsPathRooted($target)) {' + #13#10 + - ' $target = Join-Path -Path (Split-Path -Parent $source) -ChildPath $target' + #13#10 + - ' }' + #13#10 + - ' $target = [System.IO.Path]::GetFullPath($target)' + #13#10 + - ' $sourceRoot = [System.IO.Path]::GetFullPath($sourceRoot)' + #13#10 + - ' $destRoot = [System.IO.Path]::GetFullPath($destRoot)' + #13#10 + - ' if ($target.StartsWith($sourceRoot, [System.StringComparison]::OrdinalIgnoreCase)) {' + #13#10 + - ' $suffix = $target.Substring($sourceRoot.Length).TrimStart(''\'')' + #13#10 + - ' if ([string]::IsNullOrEmpty($suffix)) {' + #13#10 + - ' $target = $destRoot' + #13#10 + - ' } else {' + #13#10 + - ' $target = Join-Path -Path $destRoot -ChildPath $suffix' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - ' if (-not (Test-Path -LiteralPath $target -PathType Container)) { throw "Junction targets must be directories." }' + #13#10 + - ' $command = ''cmd.exe /C mklink /J "'' + $dest.Replace(''"'', ''""'') + ''" "'' + $target.Replace(''"'', ''""'') + ''"''' + #13#10 + - ' if (Test-Path -LiteralPath $dest) {' + #13#10 + - ' Remove-Item -LiteralPath $dest -Recurse -Force -ErrorAction Stop' + #13#10 + - ' }' + #13#10 + - ' New-Item -ItemType Junction -Path $dest -Target $target -Force -ErrorAction Stop | Out-Null' + #13#10 + - ' Set-Content -LiteralPath $resultFile -Value @(''status: ok'', ''command: '' + $command) -Encoding UTF8' + #13#10 + - ' exit 0' + #13#10 + - '} catch {' + #13#10 + - ' if ([string]::IsNullOrWhiteSpace($command)) {' + #13#10 + - ' $command = ''cmd.exe /C mklink /J "'' + $dest.Replace(''"'', ''""'') + ''" ""''' + #13#10 + - ' }' + #13#10 + - ' Set-Content -LiteralPath $resultFile -Value @(''status: fail'', ''command: '' + $command, ''reason: '' + $_.Exception.Message) -Encoding UTF8' + #13#10 + - ' exit 1' + #13#10 + - '}'; - - SaveStringToFile(ScriptFile, ScriptText, False); - if not Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - begin - AppendSkippedSymlink(NormalizePath(SourcePath), BuildMklinkJunctionCommand(NormalizePath(DestPath), ''), 'Unable to launch PowerShell to recreate junction.'); - DeleteFile(ScriptFile); - Exit; - end; - - if not LoadStringFromFile(ResultFile, ScriptOutput) then - begin - AppendSkippedSymlink(NormalizePath(SourcePath), BuildMklinkJunctionCommand(NormalizePath(DestPath), ''), 'No junction migration result was returned.'); - DeleteFile(ScriptFile); - DeleteFile(ResultFile); - Exit; - end; - - OutputText := Trim(String(ScriptOutput)); - CommandLine := GetLegacySettingValue(OutputText, 'command'); - FailureReason := GetLegacySettingValue(OutputText, 'reason'); - - if CompareText(GetLegacySettingValue(OutputText, 'status'), 'ok') = 0 then - begin - AppendInstallLog('Recreated migrated link as junction: ' + NormalizePath(DestPath)); - Result := True; - end - else - AppendSkippedSymlink(NormalizePath(SourcePath), CommandLine, FailureReason); - - DeleteFile(ScriptFile); - DeleteFile(ResultFile); -end; - -procedure MigrateReparsePointsAsJunctions( - const SourceDir, DestDir, SourceRoot, DestRoot: String -); -var - FindRec: TFindRec; - CurrentPath: String; - CurrentDestPath: String; -begin - if not FindFirst(AddBackslash(SourceDir) + '*', FindRec) then - Exit; - - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') then - begin - CurrentPath := AddBackslash(SourceDir) + FindRec.Name; - CurrentDestPath := AddBackslash(DestDir) + FindRec.Name; - - if IsReparsePoint(FindRec.Attributes) then - TryMigrateReparsePointAsJunction(CurrentPath, CurrentDestPath, SourceRoot, DestRoot) - else if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then - MigrateReparsePointsAsJunctions(CurrentPath, CurrentDestPath, SourceRoot, DestRoot); - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; -end; - -function CopyTreeWithProgress( - const SourceDir, DestDir: String; - ProgressPage: TOutputProgressWizardPage; - var CopiedCount: Integer; - const TotalCount: Integer; - const VersionLabel: String -): Boolean; -var - ResultCode: Integer; - ExecResult: Boolean; - SourceFileCount: Integer; - SourceArg: String; - DestArg: String; - ProgressSubText: String; - StepCode: String; - StepMessage: String; - StepSeconds: Integer; -begin - Result := True; - - SourceArg := NormalizePath(SourceDir); - DestArg := NormalizePath(DestDir); - AppendInstallLog('CopyTreeWithProgress (robocopy): ' + SourceArg + ' -> ' + DestArg); - - SourceFileCount := 0; - CountFilesInTree(SourceArg, SourceFileCount); - - if not ForceDirectories(DestArg) then - begin - AppendInstallLogWarn('ForceDirectories failed for ' + DestArg); - Result := False; - Exit; - end; - - if ProgressPage <> nil then - begin - if Trim(VersionLabel) <> '' then - ProgressSubText := 'Copying ' + Trim(VersionLabel) + '...' - else - ProgressSubText := 'Copying existing Node.js installations...'; - - ProgressPage.SetText( - 'Migrating Node.js versions', - ProgressSubText - ); - ProgressPage.SetProgress(CopiedCount, TotalCount); - end; - - { Drop /V: verbose lines go to a hidden console (SW_HIDE), not the wizard or install.log. } - StepCode := 'NVM4105'; - StepMessage := 'Copying a Node.js version exceeded 5 minutes.'; - StepSeconds := 300; - if StepBudgetArmed and (BudgetCode <> '') then - begin - StepCode := BudgetCode; - StepMessage := BudgetMessage; - StepSeconds := RemainingBudgetSec; - end; - ExecResult := - ExecStep( - 'robocopy.exe', - '"' + SourceArg + '" "' + DestArg + '" /E /MT:16 /R:1 /W:1 /NP /XJF /XJD /SJ', - StepCode, - StepMessage, - StepSeconds, - False, - False, - ResultCode - ); - - if not ExecResult then - begin - AppendInstallLogWarn('Robocopy execution failed for ' + SourceArg); - Result := False; - Exit; - end; - - if ResultCode >= 8 then - begin - AppendInstallLogWarn('Robocopy failed with code ' + IntToStr(ResultCode)); - AppendInstallLogWarn('Robocopy command source=' + SourceArg + ' dest=' + DestArg); - Result := False; - Exit; - end; - - MigrateReparsePointsAsJunctions(SourceArg, DestArg, SourceArg, DestArg); - - CopiedCount := CopiedCount + SourceFileCount; - if CopiedCount > TotalCount then - CopiedCount := TotalCount; - - if ProgressPage <> nil then - ProgressPage.SetProgress(CopiedCount, TotalCount); - - AppendInstallLog('CopyTreeWithProgress completed'); -end; - -#include "officialnode.iss" - -procedure MigrateNodeStorageIfNeeded(); -var - ExistingRoot: String; - TargetRoot: String; - FileCount: Integer; - CopiedCount: Integer; - ProgressPage: TOutputProgressWizardPage; - TargetExistedBefore: Boolean; - VersionDirCount: Integer; - CopiedVersionDirCount: Integer; - SourceVersionDir: String; - TargetVersionDir: String; - FindRec: TFindRec; -begin - AppendInstallLog('MigrateNodeStorageIfNeeded: begin'); - ResetSkippedSymlinkLog(); - AppendInstallLog('MigrateNodeStorageIfNeeded: IsPreV2Upgrade=' + IntToStr(Integer(IsPreV2Upgrade))); - AppendInstallLog('MigrateNodeStorageIfNeeded: LegacyInstallDir=' + LegacyInstallDir); - AppendInstallLog('MigrateNodeStorageIfNeeded: LegacySettingsRoot=' + LegacySettingsRoot); - AppendInstallLog('MigrateNodeStorageIfNeeded: PreviousInstallRoot=' + PreviousInstallRoot); - ExistingRoot := Trim(PreviousInstallRoot); - if ExistingRoot = '' then - begin - AppendInstallLog('MigrateNodeStorageIfNeeded: PreviousInstallRoot empty, querying existing install root'); - if not GetExistingInstallRoot(ExistingRoot) then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: no existing install root found, exiting'); - Exit; - end; - end; - - TargetRoot := GetInstallRoot(''); - AppendInstallLog('MigrateNodeStorageIfNeeded: resolved source=' + ExistingRoot); - AppendInstallLog('MigrateNodeStorageIfNeeded: resolved target=' + TargetRoot); - - if CompareText(NormalizePath(ExistingRoot), NormalizePath(TargetRoot)) = 0 then - begin - AppendInstallLog('MigrateNodeStorageIfNeeded: source and target are the same, exiting'); - Exit; - end; - - if not DirExists(ExistingRoot) then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: source directory does not exist, exiting'); - Exit; - end; - - FileCount := 0; - VersionDirCount := 0; - CountFilesInLegacyVersionDirectories(ExistingRoot, FileCount, VersionDirCount); - AppendInstallLog('MigrateNodeStorageIfNeeded: version directory count=' + IntToStr(VersionDirCount)); - AppendInstallLog('MigrateNodeStorageIfNeeded: source version file count=' + IntToStr(FileCount)); - - if VersionDirCount = 0 then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: no version directories (v*) found, skipping migration'); - Exit; - end; - - if FileCount = 0 then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: version directories are empty, skipping migration'); - Exit; - end; - - ProgressPage := CreateOutputProgressPage( - 'Migrating Node.js versions', - 'Moving installed Node.js versions to your new storage directory.' - ); - ProgressPage.SetProgress(0, FileCount); - ProgressPage.Show; - - TargetExistedBefore := DirExists(TargetRoot); - AppendInstallLog('MigrateNodeStorageIfNeeded: target existed before copy=' + IntToStr(Integer(TargetExistedBefore))); - CopiedCount := 0; - CopiedVersionDirCount := 0; - try - if not FindFirst(AddBackslash(ExistingRoot) + '*', FindRec) then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: no source entries found during copy, aborting migration'); - if not TargetExistedBefore then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: cleaning up newly-created target root after failure'); - DelTree(TargetRoot, True, True, True); - end; - MsgBox( - 'Node.js versions could not be fully migrated to the new storage directory.' + #13#10 + - 'The destination has been cleaned up. Your versions remain at:' + #13#10 + ExistingRoot, - mbError, - MB_OK - ); - end - else - begin - try - repeat - if (FindRec.Name <> '.') and (FindRec.Name <> '..') and - ((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) and - IsLegacyVersionDirectoryName(FindRec.Name) then - begin - SourceVersionDir := AddBackslash(ExistingRoot) + FindRec.Name; - TargetVersionDir := AddBackslash(TargetRoot) + FindRec.Name; - AppendInstallLog('MigrateNodeStorageIfNeeded: copying version directory ' + FindRec.Name); - - if not CopyTreeWithProgress(SourceVersionDir, TargetVersionDir, ProgressPage, CopiedCount, FileCount, FindRec.Name) then - begin - if InstallerHardFailed then - Exit; - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: copy failed after ' + IntToStr(CopiedCount) + ' files'); - if not TargetExistedBefore then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: cleaning up newly-created target root after failure'); - DelTree(TargetRoot, True, True, True); - end; - MsgBox( - 'Node.js versions could not be fully migrated to the new storage directory.' + #13#10 + - 'The destination has been cleaned up. Your versions remain at:' + #13#10 + ExistingRoot, - mbError, - MB_OK - ); - Exit; - end; - - CopiedVersionDirCount := CopiedVersionDirCount + 1; - end; - until not FindNext(FindRec); - finally - FindClose(FindRec); - end; - - if CopiedVersionDirCount <> VersionDirCount then - begin - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: copied version directory count mismatch'); - AppendInstallLogWarn('MigrateNodeStorageIfNeeded: expected=' + IntToStr(VersionDirCount) + ' actual=' + IntToStr(CopiedVersionDirCount)); - end; - - MigrationSource := ExistingRoot; - MigrationDest := TargetRoot; - MigrationPerformed := True; - AppendInstallLog('MigrateNodeStorageIfNeeded: migration succeeded, deleting source root'); - DelTree(ExistingRoot, True, True, True); - end; - finally - AppendInstallLog('MigrateNodeStorageIfNeeded: end'); - FlushSkippedSymlinkLog(); - FlushInstallLog(); - ProgressPage.Hide; - end; -end; - -function GetCoreVersion(const VersionText: String): String; -var - SeparatorPos: Integer; -begin - Result := Trim(VersionText); - - SeparatorPos := Pos('-', Result); - if SeparatorPos > 0 then - Result := Copy(Result, 1, SeparatorPos - 1); - - SeparatorPos := Pos('+', Result); - if SeparatorPos > 0 then - Result := Copy(Result, 1, SeparatorPos - 1); -end; - -function NextVersionPart(const VersionText: String; var Position: Integer): Integer; -var - StartPos: Integer; - PartText: String; -begin - while (Position <= Length(VersionText)) and - not ((VersionText[Position] >= '0') and (VersionText[Position] <= '9')) do - Position := Position + 1; - - if Position > Length(VersionText) then - begin - Result := -1; - Exit; - end; - - StartPos := Position; - while (Position <= Length(VersionText)) and - ((VersionText[Position] >= '0') and (VersionText[Position] <= '9')) do - Position := Position + 1; - - PartText := Copy(VersionText, StartPos, Position - StartPos); - Result := StrToIntDef(PartText, 0); -end; - -function GetMajorVersion(const VersionText: String): Integer; -var - Position: Integer; - CoreVersion: String; -begin - CoreVersion := GetCoreVersion(VersionText); - Position := 1; - Result := NextVersionPart(CoreVersion, Position); -end; - -function CompareVersions(const LeftVersion, RightVersion: String): Integer; -var - LeftPos, RightPos: Integer; - LeftPart, RightPart: Integer; - LeftCore, RightCore: String; -begin - LeftCore := GetCoreVersion(LeftVersion); - RightCore := GetCoreVersion(RightVersion); - LeftPos := 1; - RightPos := 1; - - while True do - begin - LeftPart := NextVersionPart(LeftCore, LeftPos); - RightPart := NextVersionPart(RightCore, RightPos); - - if (LeftPart = -1) and (RightPart = -1) then - begin - Result := 0; - Exit; - end; - - if LeftPart = -1 then - LeftPart := 0; - if RightPart = -1 then - RightPart := 0; - - if LeftPart < RightPart then - begin - Result := -1; - Exit; - end; - - if LeftPart > RightPart then - begin - Result := 1; - Exit; - end; - end; -end; - -function GetAutoDetectRegistryValue(Param: String): String; -begin - Result := '.nvmrc' + #0 + '.node-version' + #0 + 'package.json'; - if (PreferencesPage <> nil) and not PreferencesPage.Values[1] then - Result := ''; -end; - -function GetAutoInstallRegistryValue(Param: String): String; -begin - Result := '0'; - if (PreferencesPage <> nil) and PreferencesPage.Values[2] then - Result := '1'; -end; - -function GetAutoInstallPromptRegistryValue(Param: String): String; -begin - Result := '0'; - if (PreferencesPage <> nil) and PreferencesPage.Values[3] then - Result := '1'; -end; - -function GetAllowInsecureDownloadsRegistryValue(Param: String): String; -begin - Result := '0'; - if (PreferencesPage <> nil) and not PreferencesPage.Values[4] then - Result := '1'; -end; - -function GetCurrentDateTimeRegistryValue(Param: String): String; -begin - Result := GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':'); -end; - -function IsValidEmail(const Email: string): Boolean; -var - AtPos, DotPos: Integer; -begin - AtPos := Pos('@', Email); - DotPos := Pos('.', Copy(Email, AtPos + 1, Length(Email))); - if AtPos > 0 then - DotPos := DotPos + AtPos; - Result := (AtPos > 1) and (DotPos > AtPos + 1) and (DotPos < Length(Email)); -end; - -function GetSubscriptionEmail(): String; -begin - Result := ''; - - if EmailEdit = nil then - Exit; - - Result := Trim(EmailEdit.Text); - if (Result = '') or (CompareText(Result, EmailPlaceholder) = 0) then - Result := ''; -end; - -function ShouldRunSubscriptionCommand(): Boolean; -begin - Result := GetSubscriptionEmail() <> ''; -end; - -procedure EmailEditEnter(Sender: TObject); -begin - if EmailFirstFocus then - begin - EmailFirstFocus := False; - Exit; - end; - if EmailEdit.Text = EmailPlaceholder then - EmailEdit.Text := ''; -end; - -procedure EmailEditExit(Sender: TObject); -begin - if Trim(EmailEdit.Text) = '' then - EmailEdit.Text := EmailPlaceholder; -end; - -procedure ReadyCertifiedLinkClick(Sender: TObject; const Link: String; LinkType: TSysLinkType); forward; - -function GetReadyAnnouncementEmail(): String; -begin - if (EmailEdit <> nil) and (Trim(EmailEdit.Text) <> '') and (Trim(EmailEdit.Text) <> EmailPlaceholder) then - Result := Trim(EmailEdit.Text) - else - Result := 'Not provided'; -end; - -procedure LayoutReadySummaryControls(); -var - Y: Integer; - I: Integer; - InnerWidth: Integer; - LeftPos: Integer; - AreaTop: Integer; -begin - if (ReadyEventNote = nil) or (ReadySummaryTop = nil) then - Exit; - - AreaTop := WizardForm.ReadyLabel.Top; - LeftPos := WizardForm.ReadyMemo.Left; - InnerWidth := WizardForm.ReadyMemo.Width; - if InnerWidth < ScaleX(200) then - InnerWidth := ScaleX(200); - - ReadyEventNote.Left := LeftPos; - ReadyEventNote.Top := AreaTop; - ReadyEventNote.Width := InnerWidth; - Y := ReadyEventNote.Top + ReadyEventNote.Height + ScaleY(8); - - ReadySummaryTop.Left := LeftPos; - ReadySummaryTop.Top := Y; - ReadySummaryTop.Width := InnerWidth; - Y := ReadySummaryTop.Top + ReadySummaryTop.Height + ScaleY(6); - - for I := 0 to 4 do - begin - ReadyPrefCheck[I].Left := LeftPos; - ReadyPrefCheck[I].Width := InnerWidth; - ReadyPrefCheck[I].Top := Y; - Y := Y + ReadyPrefCheck[I].Height + ScaleY(1); - end; - - ReadySummaryBottom.Left := LeftPos; - ReadySummaryBottom.Top := Y + ScaleY(8); - ReadySummaryBottom.Width := InnerWidth; - ReadySummaryCertified.Left := LeftPos; - ReadySummaryCertified.Top := ReadySummaryBottom.Top + ReadySummaryBottom.Height + ScaleY(12); - ReadySummaryCertified.Width := InnerWidth; -end; - -procedure RefreshReadySummary(); -var - Mode: String; - SummaryText: String; - I: Integer; -begin - if (ReadyEventNote = nil) or (ReadySummaryTop = nil) then - Exit; - - ReadyEventNote.Width := WizardForm.ReadyMemo.Width; - ReadySummaryTop.Width := WizardForm.ReadyMemo.Width; - ReadySummaryBottom.Width := WizardForm.ReadyMemo.Width; - ReadySummaryCertified.Width := WizardForm.ReadyMemo.Width; - - ReadyEventNote.Caption := - 'You may be prompted to allow NVM for Windows to register as a Windows event source.'; - - if (OperatingModePage <> nil) and OperatingModePage.Values[1] then - Mode := 'Link' - else - Mode := 'Shim (recommended)'; - - SummaryText := - 'NVM for Windows will be installed with the following settings:' + #13#10 + #13#10 + - 'Node.js storage: ' + GetInstallRoot('') + #13#10 + - 'Operating mode: ' + Mode + #13#10 + - 'Announcements email: ' + GetReadyAnnouncementEmail(); - - if IsPreV2Upgrade then - SummaryText := SummaryText + #13#10 + #13#10 + - 'A prior NVM for Windows installation was detected. Your settings and Node.js versions will be migrated.'; - - if OfficialNodeDetected and DeferOfficialNodeOnV2Upgrade then - SummaryText := SummaryText + #13#10 + #13#10 + - OfficialNodeSummary() + #13#10 + - 'The existing Node.js installation is left in place. After setup, Notification Center reminds you to uninstall it.' - else if OfficialNodeDetected then - SummaryText := SummaryText + #13#10 + #13#10 + - OfficialNodeSummary() + #13#10 + - 'Official Node.js action: ' + SelectedOfficialNodeAction() + #13#10 + - 'Official Node.js path: ' + OfficialNodePath; - - ReadySummaryTop.Caption := SummaryText; - - if PreferencesPage <> nil then - begin - for I := 0 to 4 do - ReadyPrefCheck[I].Checked := PreferencesPage.Values[I]; - end; - - ReadySummaryBottom.Caption := - 'Community builds install in the per-user LocalAppData folder.'; - ReadySummaryCertified.Caption := - 'For IT-managed Program Files installations, MSI, or Intune deployment, use Certified Builds.'; - - LayoutReadySummaryControls(); -end; - -procedure ReadyCertifiedLinkClick(Sender: TObject; const Link: String; LinkType: TSysLinkType); -var - ResultCode: Integer; -begin - if Trim(Link) = '' then - Exit; - ShellExec('open', Link, '', '', SW_SHOWNORMAL, ewNoWait, ResultCode); -end; - -procedure CreateReadySummaryControls(); -var - Host: TWinControl; - I: Integer; -begin - WizardForm.ReadyLabel.Visible := False; - WizardForm.ReadyMemo.Visible := False; - WizardForm.ReadyMemo.ScrollBars := ssVertical; - WizardForm.ReadyMemo.WordWrap := True; - - Host := WizardForm.ReadyMemo.Parent; - - ReadyEventNote := TNewStaticText.Create(WizardForm); - ReadyEventNote.Parent := Host; - ReadyEventNote.AutoSize := True; - ReadyEventNote.WordWrap := True; - ReadyEventNote.Color := $00CCFFFF; - ReadyEventNote.Font.Style := [fsBold]; - ReadyEventNote.Caption := ''; - - ReadySummaryTop := TNewStaticText.Create(WizardForm); - ReadySummaryTop.Parent := Host; - ReadySummaryTop.AutoSize := True; - ReadySummaryTop.WordWrap := True; - ReadySummaryTop.Caption := ''; - - for I := 0 to 4 do - begin - ReadyPrefCheck[I] := TNewCheckBox.Create(WizardForm); - ReadyPrefCheck[I].Parent := Host; - ReadyPrefCheck[I].Height := ScaleY(18); - ReadyPrefCheck[I].Enabled := False; - ReadyPrefCheck[I].TabStop := False; - end; - ReadyPrefCheck[0].Caption := 'Keep downloaded Node.js setup files (cache for reinstall)'; - ReadyPrefCheck[1].Caption := 'Auto-detect Node.js version, e.g. .nvmrc, .node-version, package.json'; - ReadyPrefCheck[2].Caption := 'Auto-install missing Node.js versions'; - ReadyPrefCheck[3].Caption := 'Prompt before installing'; - ReadyPrefCheck[4].Caption := 'Validate TLS/SSL certificates'; - - ReadySummaryBottom := TNewStaticText.Create(WizardForm); - ReadySummaryBottom.Parent := Host; - ReadySummaryBottom.AutoSize := True; - ReadySummaryBottom.WordWrap := True; - ReadySummaryBottom.Font.Style := [fsBold]; - ReadySummaryBottom.Caption := ''; - - ReadySummaryCertified := TNewLinkLabel.Create(WizardForm); - ReadySummaryCertified.Parent := Host; - ReadySummaryCertified.AutoSize := True; - ReadySummaryCertified.Font.Style := [fsBold]; - ReadySummaryCertified.OnLinkClick := @ReadyCertifiedLinkClick; - ReadySummaryCertified.Caption := ''; -end; - -procedure CurPageChanged(CurPageID: Integer); -begin - if (EmailPage <> nil) and (CurPageID = EmailPage.ID) then - EmailFirstFocus := True; - - if (StoragePermissionsPage <> nil) and (CurPageID = StoragePermissionsPage.ID) and - (StoragePermissionsPathLabel <> nil) then - StoragePermissionsPathLabel.Caption := 'Selected path:' + #13#10 + GetInstallRoot(''); - - if CurPageID = wpReady then - RefreshReadySummary(); -end; - -function DefaultAppDataInstallRoot(): String; -begin - Result := GetRealInstallRoot(''); -end; - -function IsPathUnderSafeManagedRoot(const PathValue: String): Boolean; -var - PathNorm: String; - Candidate: String; -begin - Result := False; - PathNorm := LowerCase(NormalizePath(PathValue)); - if PathNorm = '' then - Exit; - - Candidate := LowerCase(NormalizePath(GetRealLocalAppData(''))); - if (Candidate <> '') and ((PathNorm = Candidate) or (Pos(Candidate + '\', PathNorm) = 1)) then - begin - Result := True; - Exit; - end; - - Candidate := LowerCase(NormalizePath(GetEnv('USERPROFILE'))); - if (Candidate <> '') and ((PathNorm = Candidate) or (Pos(Candidate + '\', PathNorm) = 1)) then - begin - Result := True; - Exit; - end; - - Candidate := LowerCase(NormalizePath(ExpandConstant('{pf}'))); - if (Candidate <> '') and ((PathNorm = Candidate) or (Pos(Candidate + '\', PathNorm) = 1)) then - begin - Result := True; - Exit; - end; - - Candidate := LowerCase(NormalizePath(ExpandConstant('{pf32}'))); - if (Candidate <> '') and ((PathNorm = Candidate) or (Pos(Candidate + '\', PathNorm) = 1)) then - Result := True; -end; - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - Result := False; - if (OfficialNodePage <> nil) and (PageID = OfficialNodePage.ID) then - begin - Result := WizardSilent or (not OfficialNodeDetected); - Exit; - end; - if (StoragePermissionsPage <> nil) and (PageID = StoragePermissionsPage.ID) then - begin - if WizardSilent then - begin - Result := True; - Exit; - end; - Result := IsPathUnderSafeManagedRoot(GetInstallRoot('')); - end; -end; - -function NextButtonClick(CurPageID: Integer): Boolean; -var - AppDataRoot: String; -begin - Result := True; - if (StoragePermissionsPage = nil) or (CurPageID <> StoragePermissionsPage.ID) then - Exit; - - if StoragePermissionsPage.Values[0] then - AclStorageChoice := 0 - else if StoragePermissionsPage.Values[1] then - begin - AclStorageChoice := 1; - AppDataRoot := DefaultAppDataInstallRoot(); - NodeStoragePage.Values[0] := AppDataRoot; - AppendInstallLog('StoragePermissionsPage: user chose AppData root ' + AppDataRoot); - end - else - AclStorageChoice := 2; -end; - -function CheckRuntimeAclsOk(const InstallRoot: String): Boolean; -var - ResultCode: Integer; - Args: String; -begin - Result := False; - if Trim(InstallRoot) = '' then - Exit; - if not FileExists(ExpandConstant('{app}\{#Alias}.exe')) then - Exit; - - Args := '--check-runtime-acls "' + InstallRoot + '"'; - if ExecStep( - ExpandConstant('{app}\{#Alias}.exe'), - Args, - 'NVM4103', - 'Checking Node.js storage permissions exceeded 5 minutes.', - 300, - True, - False, - ResultCode - ) then - Result := (ResultCode = 0) - else - AppendInstallLogWarn('CheckRuntimeAclsOk: failed to start nvm --check-runtime-acls'); -end; - -function AllowDegradedAclsParam(): Boolean; -var - Raw: String; -begin - Raw := Trim(ExpandConstant('{param:ALLOWDEGRADEDACLS}')); - Result := (Raw = '1') or (CompareText(Raw, 'true') = 0) or (CompareText(Raw, 'yes') = 0); -end; - -procedure ApplySilentInstallRootPolicy(); -var - Candidate: String; - AppDataRoot: String; -begin - SilentForcedAppDataFrom := ''; - if not WizardSilent then - Exit; - - Candidate := NormalizePath(GetInstallRoot('')); - AppDataRoot := NormalizePath(DefaultAppDataInstallRoot()); - if Candidate = '' then - begin - NodeStoragePage.Values[0] := DefaultAppDataInstallRoot(); - Exit; - end; - - if CompareText(Candidate, AppDataRoot) = 0 then - Exit; - - if IsPathUnderSafeManagedRoot(Candidate) then - begin - AppendInstallLog('Silent ACL policy: keeping safe InstallRoot ' + Candidate); - Exit; - end; - - { nvm.exe is on disk in ssPostInstall; probe + optional current-token harden. } - if CheckRuntimeAclsOk(Candidate) then - begin - AppendInstallLog('Silent ACL policy: keeping InstallRoot after successful check/repair: ' + Candidate); - Exit; - end; - - AppendInstallLogWarn('Silent ACL policy: InstallRoot unsafe, forcing AppData: ' + Candidate + ' -> ' + AppDataRoot); - SilentForcedAppDataFrom := Candidate; - if Trim(PreviousInstallRoot) = '' then - PreviousInstallRoot := Candidate - else if CompareText(NormalizePath(PreviousInstallRoot), Candidate) <> 0 then - begin - { Prefer migrating the unsafe custom root the user had configured. } - PreviousInstallRoot := Candidate; - end; - NodeStoragePage.Values[0] := DefaultAppDataInstallRoot(); - RegWriteStringValue(HKCU, '{#RegistryKey}', 'InstallRoot', DefaultAppDataInstallRoot()); -end; - -procedure SetRuntimeACLDegradedFlag(const Degraded: Boolean); -begin - RuntimeACLDegraded := Degraded; - if Degraded then - RegWriteDWordValue(HKCU, '{#RegistryKey}', 'RuntimeACLDegraded', 1) - else - RegWriteDWordValue(HKCU, '{#RegistryKey}', 'RuntimeACLDegraded', 0); -end; - -procedure RepairRuntimeAclsPostInstall(); -var - ResultCode: Integer; - ElevatedExecOk: Boolean; - NvmExe: String; -begin - NvmExe := ExpandConstant('{app}\{#Alias}.exe'); - if not FileExists(NvmExe) then - begin - AppendInstallLogWarn('RepairRuntimeAclsPostInstall: nvm.exe missing'); - Exit; - end; - - { Current-token repair first (owner WRITE_DAC / already elevated). } - if ExecStep(NvmExe, '--repair-runtime-acls', 'NVM4112', 'Repairing Node.js storage permissions exceeded its time limit.', WalkTimeoutSec(CountInstalledNodeVersions(GetInstallRoot(''))), True, False, ResultCode) then - begin - if ResultCode = 0 then - begin - AppendInstallLog('Runtime ACLs repaired with current token'); - SetRuntimeACLDegradedFlag(False); - Exit; - end; - AppendInstallLogWarn('Current-token ACL repair exit=' + IntToStr(ResultCode)); - end - else - AppendInstallLogWarn('Current-token ACL repair failed to start'); - - if WizardSilent then - begin - if AllowDegradedAclsParam() then - begin - AppendInstallLogWarn('Silent ACL repair incomplete; /ALLOWDEGRADEDACLS set β€” continuing degraded'); - SetRuntimeACLDegradedFlag(True); - Exit; - end; - RaiseException( - 'Node.js storage ACL harden failed during silent install. ' + - 'Re-run the interactive installer, choose AppData storage, or pass /ALLOWDEGRADEDACLS=1. ' + - 'After install you can also run: nvm doctor --autofix' - ); - end; - - { Interactive: elevate only when user chose Harden (default). } - if AclStorageChoice = 2 then - begin - AppendInstallLogWarn('User chose keep-path; leaving ACLs degraded'); - SetRuntimeACLDegradedFlag(True); - Exit; - end; - - ElevatedExecOk := ExecStep( - NvmExe, - '--repair-runtime-acls', - 'NVM4112', - 'Repairing Node.js storage permissions exceeded its time limit.', - WalkTimeoutSec(CountInstalledNodeVersions(GetInstallRoot(''))), - True, - True, - ResultCode - ); - - if not ElevatedExecOk then - begin - if ResultCode = 1223 then - AppendInstallLogWarn('Runtime ACL repair skipped: UAC prompt canceled') - else - AppendInstallLogWarn( - 'Runtime ACL repair skipped: elevation failed (system error ' + IntToStr(ResultCode) + ')' - ); - SetRuntimeACLDegradedFlag(True); - Exit; - end; - - if ResultCode <> 0 then - begin - AppendInstallLogWarn( - 'Runtime ACL repair returned exit code ' + IntToStr(ResultCode) + - ' β€” run `nvm doctor --autofix` after install if npm reports NVM4305' - ); - SetRuntimeACLDegradedFlag(True); - end - else - begin - AppendInstallLog('Runtime ACLs repaired successfully (elevated)'); - SetRuntimeACLDegradedFlag(False); - end; -end; - -procedure NotifySilentStorageMigration(); -var - ResultCode: Integer; - Args: String; -begin - if Trim(SilentForcedAppDataFrom) = '' then - Exit; - Args := - '--notify-storage-migrated "' + SilentForcedAppDataFrom + '" "' + - DefaultAppDataInstallRoot() + '"'; - if not Exec( - ExpandConstant('{app}\{#Alias}.exe'), - Args, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - AppendInstallLogWarn('Silent storage migration toast failed to start') - else if ResultCode <> 0 then - AppendInstallLogWarn('Silent storage migration toast exit=' + IntToStr(ResultCode)) - else - AppendInstallLog('Silent storage migration toast sent'); -end; - -procedure NotifyOfficialNodeLeftInPlace(); -var - ResultCode: Integer; -begin - if (not DeferOfficialNodeOnV2Upgrade) or (not OfficialNodeDetected) then - Exit; - if not Exec( - ExpandConstant('{app}\{#Alias}.exe'), - '--notify-official-node "' + OfficialNodePath + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - AppendInstallLogWarn('Official Node.js reminder failed to start') - else if ResultCode <> 0 then - AppendInstallLogWarn('Official Node.js reminder exit=' + IntToStr(ResultCode)) - else - AppendInstallLog('Official Node.js reminder sent for ' + OfficialNodePath); -end; - -procedure UpdateAutoInstallPromptState(); -var - PromptEnabled: Boolean; -begin - if PreferencesPage = nil then Exit; - PromptEnabled := PreferencesPage.Values[2]; - - PreferencesPage.CheckListBox.ItemEnabled[3] := PromptEnabled; - if not PromptEnabled then - begin - PreferencesPage.Values[3] := False; - end; - - PreferencesPage.CheckListBox.Invalidate; -end; - -procedure PreferencesCheckChanged(Sender: TObject); -begin - UpdateAutoInstallPromptState(); -end; - -function IsBuildArchitectureCompatible(): Boolean; -begin - { Native OS arch must match the binaries in this setup (not the emulated process arch). } -#if Architecture == "arm64" - Result := IsArm64; -#else - Result := IsX64Compatible and (not IsArm64); -#endif -end; - -function ArchitectureMismatchMessage(): String; -begin -#if Architecture == "arm64" - Result := - 'This NVM for Windows installer is built for ARM64.' + #13#10#13#10 + - 'This computer is not native ARM64.' + #13#10#13#10 + - 'Download the amd64 (x64) installer instead, then try again.'; -#else - Result := - 'This NVM for Windows installer is built for amd64 (x64).' + #13#10#13#10 + - 'This computer appears to be ARM64 or another unsupported architecture.' + #13#10#13#10 + - 'Download the ARM64 installer instead, then try again.'; -#endif -end; - -function InitializeSetup(): Boolean; -var - ExistingVersion: String; - InstallingVersion: String; - ExistingMajorVersion: Integer; - ExistingInstallDetected: Boolean; - V1NodeRoot: String; - ForcedDir: String; - RequestedDir: String; - Message: String; -begin - Result := True; - RemoveLegacyTasks := False; - IsPreV2Upgrade := False; - IsExistingV2Upgrade := False; - DeferOfficialNodeOnV2Upgrade := False; - IsConcreteLegacyUpgrade := False; - - if not IsBuildArchitectureCompatible() then - begin - MsgBox(ArchitectureMismatchMessage(), mbCriticalError, MB_OK); - Result := False; - Exit; - end; - - ResetWizardDefaults(); - LegacyInstallDir := Trim(GetEnv('NVM_HOME')); - LoadLegacySettings(); - PreviousInstallRoot := ''; - GetExistingInstallRoot(PreviousInstallRoot); - CaptureLegacyActiveVersionFromSymlink(); - InstallingVersion := '{#Version}'; - ExistingInstallDetected := IsExistingInstallDetected(); - - if ExistingInstallDetected then - begin - ExistingVersion := GetExistingInstallVersion(); - - if CompareVersions(ExistingVersion, InstallingVersion) > 0 then - begin - MsgBox( - 'A newer version of NVM for Windows is already installed (' + ExistingVersion + ').' + #13#10#13#10 + - 'You are trying to install an older version (' + InstallingVersion + ').' + #13#10#13#10 + - 'Uninstall the newer version first, then install this older version.', - mbCriticalError, - MB_OK - ); - Result := False; - Exit; - end; - - ExistingMajorVersion := GetMajorVersion(ExistingVersion); - - if (ExistingMajorVersion >= 0) and (ExistingMajorVersion < 2) then - begin - IsPreV2Upgrade := True; - IsConcreteLegacyUpgrade := True; - RemoveLegacyTasks := True; - V1NodeRoot := GetLegacyV1NodeStorageRoot(); - if V1NodeRoot <> '' then - PreviousInstallRoot := V1NodeRoot; - end - else if ExistingMajorVersion >= 2 then - begin - IsExistingV2Upgrade := True; - DeferOfficialNodeOnV2Upgrade := True; - end; - - if not ConfirmExistingInstallMigration(ExistingVersion) then - begin - Result := False; - Exit; - end; - - if Result and (not IsPreV2Upgrade) and HasExistingV2Preferences() then - LoadExistingV2WizardDefaults(); - end; - - // Pure v1 install: no current v2 markers exist, but legacy settings or uninstall - // metadata are still present. Detect it here and enable migration so installed - // Node versions are moved without double-prompting current v2 installs. - ExistingVersion := GetLegacyV1InstalledVersion(); - if (not IsPreV2Upgrade) and - (not ExistingInstallDetected) and - (LegacyInstallDir <> '') and - DirExists(LegacyInstallDir) and - (LegacySettingsLoaded or (ExistingVersion <> '')) then - begin - IsPreV2Upgrade := True; - RemoveLegacyTasks := True; - V1NodeRoot := GetLegacyV1NodeStorageRoot(); - if V1NodeRoot <> '' then - PreviousInstallRoot := V1NodeRoot; - - if ExistingVersion <> '' then - IsConcreteLegacyUpgrade := True; - - if not ConfirmExistingInstallMigration(ExistingVersion) then - Result := False; - end; - - if Result and (not IsPreV2Upgrade) and HasExistingV2Preferences() then - LoadExistingV2WizardDefaults(); - - // Silent /DIR overrides are rejected before wizard UI (Application event NVM4100 / EventId 4100). - // Do not call WizardDirValue here. WizardForm does not exist during InitializeSetup, - // and a silent upgrade with no /DIR crashes on that read. - if Result and WizardSilent then - begin - ForcedDir := GetRealProgramRoot(''); - RequestedDir := ExpandConstant('{param:DIR|}'); - if (RequestedDir <> '') and - (CompareText(NormalizePath(RequestedDir), NormalizePath(ForcedDir)) <> 0) then - begin - Message := - 'NVM4100: {#Name} {#Version} silent install aborted. ' + - 'Custom program directories are not allowed (/DIR is ignored/rejected). ' + - 'Requested "' + RequestedDir + '"; required program root is "' + ForcedDir + '" under LocalAppData. ' + - 'To choose where Node.js versions are stored, set InstallRoot (wizard Node.js Storage page, or nvm config after install) β€” do not use /DIR for the program root.'; - WriteApplicationEventError(4100, Message); - Log(Message); - Result := False; - end; - end; -end; - -function PrepareToInstall(var NeedsRestart: Boolean): String; -var - ShimDir: String; - DataShimDir: String; - ResultCode: Integer; - UserGrant: String; -begin - Result := ''; - NeedsRestart := False; - ShimDir := ExpandConstant('{app}\.shim'); - DataShimDir := AddBackslash(GetDataRoot('')) + '.shim'; - - // Runtime LockShimDirectory makes .shim a protected read-only DACL. - // When DataRoot == {app}, reinstall extracts node.exe here and Inno reports - // "create a file in the destination directory: Access is denied." - // User still has WRITE_DAC, so icacls /reset works without elevation. - // When DataRoot differs, unlock both ProgramRoot and DataRoot shim dirs. - if DirExists(ShimDir) then - begin - if Exec( - ExpandConstant('{sys}\icacls.exe'), - '"' + ShimDir + '" /reset /T /C /Q', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('PrepareToInstall: reset .shim ACL, icacls exit=' + IntToStr(ResultCode)) - else - Log('PrepareToInstall: icacls /reset failed to start for ' + ShimDir); - - UserGrant := '"' + ShimDir + '" /grant:r "' + GetUserNameString + ':(OI)(CI)(F)" /T /C /Q'; - if Exec( - ExpandConstant('{sys}\icacls.exe'), - UserGrant, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('PrepareToInstall: grant .shim write, icacls exit=' + IntToStr(ResultCode)) - else - Log('PrepareToInstall: icacls /grant failed to start for ' + ShimDir); - end; - - if (CompareText(NormalizePath(ShimDir), NormalizePath(DataShimDir)) <> 0) and DirExists(DataShimDir) then - begin - if Exec( - ExpandConstant('{sys}\icacls.exe'), - '"' + DataShimDir + '" /reset /T /C /Q', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('PrepareToInstall: reset DataRoot .shim ACL, icacls exit=' + IntToStr(ResultCode)); - - UserGrant := '"' + DataShimDir + '" /grant:r "' + GetUserNameString + ':(OI)(CI)(F)" /T /C /Q'; - if Exec( - ExpandConstant('{sys}\icacls.exe'), - UserGrant, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('PrepareToInstall: grant DataRoot .shim write, icacls exit=' + IntToStr(ResultCode)); - end; -end; - -function ForceProgramRootDirectory: Boolean; -var - ForcedDir: String; - RequestedDir: String; -begin - Result := True; - ForcedDir := GetRealProgramRoot(''); - RequestedDir := WizardDirValue; - if CompareText(NormalizePath(RequestedDir), NormalizePath(ForcedDir)) = 0 then - begin - WizardForm.DirEdit.Text := ForcedDir; - Exit; - end; - - Log('Ignoring custom /DIR "' + RequestedDir + '"; program root is fixed at "' + ForcedDir + '". Customize Node storage / InstallRoot instead.'); - - // Silent mismatch already rejected in InitializeSetup; keep defensive fail path. - if WizardSilent then - begin - Result := False; - Exit; - end; - - if MsgBox( - 'NVM for Windows always installs under:' + #13#10 + ForcedDir + #13#10#13#10 + - 'The /DIR value "' + RequestedDir + '" is not supported.' + #13#10#13#10 + - 'OK = continue install and ignore /DIR.' + #13#10 + - 'Cancel = abort install.' + #13#10#13#10 + - 'Choose where Node.js versions are stored on the Node.js Storage page (or set InstallRoot after install).', - mbConfirmation, - MB_OKCANCEL - ) = IDOK then - WizardForm.DirEdit.Text := ForcedDir - else - Result := False; -end; - -procedure InitializeWizard; -begin - AclStorageChoice := 0; - SilentForcedAppDataFrom := ''; - RuntimeACLDegraded := False; - ShimFinalizeIncomplete := False; - - if not ForceProgramRootDirectory then - Abort; - - DetectOfficialNode(); - CreateOfficialNodePage(); - - if OfficialNodePage <> nil then - NodeStoragePage := CreateInputDirPage( - OfficialNodePage.ID, - 'Node.js Storage Location', - 'Select where Node.js versions will be stored.', - 'NVM for Windows will store Node.js and npm installations in this directory.', - False, - '' - ) - else - NodeStoragePage := CreateInputDirPage( - wpLicense, - 'Node.js Storage Location', - 'Select where Node.js versions will be stored.', - 'NVM for Windows will store Node.js and npm installations in this directory.', - False, - '' - ); - - NodeStoragePage.Add('Node.js storage path:'); - NodeStoragePage.Values[0] := WizardDefaultInstallRoot; - - StoragePermissionsPage := CreateInputOptionPage( - NodeStoragePage.ID, - 'Node.js storage permissions', - 'This location can be written by other Windows users.', - 'NVM blocks npm in that case (NVM4305) unless you harden permissions or use a private path.', - True, - False - ); - StoragePermissionsPage.Add('Harden permissions for this path (recommended; Windows may ask for approval)'); - StoragePermissionsPage.Add('Use the default AppData storage path instead'); - StoragePermissionsPage.Add('Keep this path and continue (may need nvm doctor --autofix later)'); - StoragePermissionsPage.Values[0] := True; - - StoragePermissionsPathLabel := TNewStaticText.Create(StoragePermissionsPage); - StoragePermissionsPathLabel.Parent := StoragePermissionsPage.Surface; - StoragePermissionsPathLabel.Left := ScaleX(0); - StoragePermissionsPathLabel.Top := ScaleY(130); - StoragePermissionsPathLabel.Width := StoragePermissionsPage.SurfaceWidth; - StoragePermissionsPathLabel.Height := ScaleY(40); - StoragePermissionsPathLabel.AutoSize := False; - StoragePermissionsPathLabel.WordWrap := True; - StoragePermissionsPathLabel.Caption := 'Selected path:' + #13#10 + WizardDefaultInstallRoot; - - OperatingModePage := CreateInputOptionPage( - StoragePermissionsPage.ID, - 'Operating Mode', - 'Select how Node.js should be made available on your system.', - 'Shim mode offers an enhanced modern workflow. Link mode offers guaranteed minimum latency with no extra features. This can be changed at any time by running "nvm use ".', - True, - False - ); - - OperatingModePage.Add('Shim'); - OperatingModePage.Add('Link (Legacy)'); - OperatingModePage.Values[1] := WizardDefaultUseLinkMode; - OperatingModePage.Values[0] := not WizardDefaultUseLinkMode; - - PreferencesPage := CreateInputOptionPage( - OperatingModePage.ID, - 'Preferences', - 'Choose your default settings', - 'These settings can be changed at any time using the "nvm config" command.', - False, - False - ); - - PreferencesPage.Add('Keep downloaded Node.js setup files (cache for reinstall)'); - PreferencesPage.Add('Auto-detect Node.js version, e.g. .nvmrc, .node-version, package.json (shim mode)'); - PreferencesPage.Add('Auto-install missing Node.js versions (nvm use, shim mode)'); - PreferencesPage.Add('Prompt before installing missing Node.js versions (shim mode)'); - PreferencesPage.Add('Require valid TLS/SSL certificates from download mirrors'); - - PreferencesPage.Values[0] := WizardDefaultCacheDownloads; - PreferencesPage.Values[1] := WizardDefaultAutoDetect; - PreferencesPage.Values[2] := WizardDefaultAutoInstall; - PreferencesPage.Values[3] := WizardDefaultAutoInstallPrompt; - PreferencesPage.Values[4] := WizardDefaultRequireTls; - PreferencesPage.CheckListBox.ItemEnabled[3] := WizardDefaultAutoInstall; - PreferencesPage.CheckListBox.OnClickCheck := @PreferencesCheckChanged; - - EmailPlaceholder := 'name@example.com'; - EmailFirstFocus := True; - - EmailPage := CreateCustomPage( - PreferencesPage.ID, - 'Author Software Updates', - 'Get details about development milestones in your inbox (optional)' - ); - - UpdateAutoInstallPromptState(); - - EmailLabel := TLabel.Create(EmailPage); - EmailLabel.Parent := EmailPage.Surface; - EmailLabel.Left := ScaleX(0); - EmailLabel.Top := ScaleY(0); - EmailLabel.Width := EmailPage.SurfaceWidth; - EmailLabel.Height := ScaleY(50); - EmailLabel.AutoSize := False; - EmailLabel.Caption := 'Be informed of development milestones, release timelines, and enterprise capabilities. Leave it blank if you do not wish to receive notifications.'; - EmailLabel.WordWrap := True; - - EmailEdit := TEdit.Create(EmailPage); - EmailEdit.Parent := EmailPage.Surface; - EmailEdit.Left := ScaleX(0); - EmailEdit.Top := ScaleY(60); - EmailEdit.Width := EmailPage.SurfaceWidth; - EmailEdit.Height := ScaleY(23); - EmailEdit.Text := EmailPlaceholder; - EmailEdit.OnEnter := @EmailEditEnter; - EmailEdit.OnExit := @EmailEditExit; - - CreateReadySummaryControls(); -end; - -function UpdateReadyMemo( - Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, - MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String -): String; -begin - { Ready page uses CreateReadySummaryControls, not this memo. } - Result := ''; -end; - -function ReplaceVarCI(const S, VarName, Value: String): String; -var - Upper: String; - P: Integer; -begin - Result := S; - Upper := UpperCase(Result); - P := Pos(UpperCase(VarName), Upper); - while P > 0 do - begin - Result := Copy(Result, 1, P - 1) + Value + Copy(Result, P + Length(VarName), Length(Result)); - Upper := UpperCase(Result); - P := Pos(UpperCase(VarName), Upper); - end; -end; - -function ExpandPathSegment(const Segment: String): String; -begin - Result := Segment; - Result := ReplaceVarCI(Result, '%USERPROFILE%', GetEnv('USERPROFILE')); - Result := ReplaceVarCI(Result, '%LOCALAPPDATA%', GetEnv('LOCALAPPDATA')); - Result := ReplaceVarCI(Result, '%APPDATA%', GetEnv('APPDATA')); - Result := ReplaceVarCI(Result, '%SYSTEMROOT%', GetEnv('SystemRoot')); - Result := ReplaceVarCI(Result, '%WINDIR%', GetEnv('SystemRoot')); - Result := ReplaceVarCI(Result, '%PROGRAMFILES%', GetEnv('ProgramFiles')); - Result := ReplaceVarCI(Result, '%PROGRAMFILES(X86)%', GetEnv('ProgramFiles(x86)')); - Result := ReplaceVarCI(Result, '%NVM_HOME%', ExpandConstant('{app}')); -end; - -procedure SplitPathString(const PathStr: String; var Segments: TArrayOfString); -var - Remaining: String; - P: Integer; - Count: Integer; - Seg: String; -begin - Count := 0; - SetArrayLength(Segments, 0); - Remaining := PathStr + ';'; - P := Pos(';', Remaining); - while P > 0 do - begin - Seg := Trim(Copy(Remaining, 1, P - 1)); - Remaining := Copy(Remaining, P + 1, Length(Remaining)); - P := Pos(';', Remaining); - if Seg = '' then Continue; - SetArrayLength(Segments, Count + 1); - Segments[Count] := Seg; - Count := Count + 1; - end; -end; - -procedure EnsureNvmPathPriority(); -var - UserPath: String; - NewPath: String; -begin - if not RegQueryStringValue(HKCU, 'Environment', 'Path', UserPath) then - UserPath := ''; - - NewPath := BuildCleanedPath(UserPath, True, True); - - RegWriteStringValue(HKCU, 'Environment', 'Path', NewPath); - BroadcastEnvironmentChange(); -end; - -// When InstallRoot is customized, DataRoot leaves {app}. Seed runtime .shim + proxy -// so PATH/.nodejs match where nvm use / reshim write shims (closes #1388). -procedure EnsureDataRootRuntimeLayout(); -var - DataRoot: String; - ProgramRoot: String; - ProgramShim: String; - DataShim: String; - ProgramProxy: String; - DataProxy: String; - ResultCode: Integer; -begin - DataRoot := NormalizePath(GetDataRoot('')); - ProgramRoot := NormalizePath(ExpandConstant('{app}')); - ProgramShim := AddBackslash(ProgramRoot) + '.shim'; - DataShim := AddBackslash(DataRoot) + '.shim'; - ProgramProxy := ExpandConstant('{app}\utils\proxy.exe'); - DataProxy := AddBackslash(DataRoot) + 'proxy.exe'; - - AppendInstallLog('EnsureDataRootRuntimeLayout: ProgramRoot=' + ProgramRoot); - AppendInstallLog('EnsureDataRootRuntimeLayout: DataRoot=' + DataRoot); - - if not ForceDirectories(DataShim) then - begin - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: could not create ' + DataShim); - Exit; - end; - - if CompareText(ProgramRoot, DataRoot) = 0 then - begin - AppendInstallLog('EnsureDataRootRuntimeLayout: DataRoot equals ProgramRoot, no seed copy needed'); - Exit; - end; - - if DirExists(ProgramShim) then - begin - if not ExecStep( - 'robocopy.exe', - '"' + ProgramShim + '" "' + DataShim + '" /E /MT:8 /R:1 /W:1 /NP /NFL /NDL /NJH /NJS', - 'NVM4109', - 'Creating the Node.js shim link exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ) then - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: robocopy seed failed to start') - else if ResultCode >= 8 then - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: robocopy seed exit=' + IntToStr(ResultCode)) - else - AppendInstallLog('EnsureDataRootRuntimeLayout: seeded DataRoot .shim from ProgramRoot'); - end - else - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: missing ProgramRoot .shim: ' + ProgramShim); - - { Hardlinks to proxy require same volume; certified also keeps proxy on DataRoot. } - if FileExists(ProgramProxy) then - begin - if not CopyFile(ProgramProxy, DataProxy, False) then - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: failed to copy proxy.exe to DataRoot') - else - AppendInstallLog('EnsureDataRootRuntimeLayout: copied proxy.exe to ' + DataProxy); - end; - - { Drop stale ProgramRoot .nodejs so PATH/docs don't point at the empty seed tree. } - if DirExists(AddBackslash(ProgramRoot) + '.nodejs') then - begin - if not Exec( - ExpandConstant('{cmd}'), - '/C rmdir "' + AddBackslash(ProgramRoot) + '.nodejs"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - AppendInstallLogWarn('EnsureDataRootRuntimeLayout: failed to remove stale ProgramRoot .nodejs') - else - AppendInstallLog('EnsureDataRootRuntimeLayout: removed stale ProgramRoot .nodejs junction'); - end; -end; - -function EnsureShimModeNodePathJunction(): Boolean; -var - NodePath: String; - ShimPath: String; - DataRoot: String; - CommandLine: String; - ResultCode: Integer; -begin - Result := True; - - if not IsShimModeSelected() then - Exit; - - DataRoot := NormalizePath(GetDataRoot('')); - NodePath := AddBackslash(DataRoot) + '.nodejs'; - ShimPath := AddBackslash(DataRoot) + '.shim'; - AppendInstallLog('EnsureShimModeNodePathJunction: source=' + ShimPath + ' target=' + NodePath); - - if not DirExists(ShimPath) then - begin - AppendInstallLogWarn('EnsureShimModeNodePathJunction: missing shim directory: ' + ShimPath); - Result := False; - Exit; - end; - - CommandLine := - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ' + - '"$target = ''' + NodePath + '''; ' + - '$source = ''' + ShimPath + '''; ' + - 'if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force -Recurse -ErrorAction Stop }; ' + - 'New-Item -ItemType Junction -Path $target -Target $source -Force -ErrorAction Stop | Out-Null; ' + - '(Get-Item -LiteralPath $target -Force -ErrorAction Stop).Attributes = ' + - '([System.IO.FileAttributes]::Hidden -bor [System.IO.FileAttributes]::Directory -bor [System.IO.FileAttributes]::ReparsePoint)"'; - - if not ExecStep(ExpandConstant('{cmd}'), CommandLine, 'NVM4109', 'Creating the Node.js shim link exceeded 3 minutes.', 180, True, False, ResultCode) then - begin - if InstallerHardFailed then - Exit; - AppendInstallLogWarn('EnsureShimModeNodePathJunction: failed to execute junction command'); - Result := False; - Exit; - end; - - if ResultCode <> 0 then - begin - AppendInstallLogWarn('EnsureShimModeNodePathJunction: mklink returned exit code ' + IntToStr(ResultCode)); - Result := False; - Exit; - end; - - AppendInstallLog('EnsureShimModeNodePathJunction: created hidden .nodejs -> .shim under DataRoot'); -end; - -procedure RegisterInstalledVersionsInWindowsApps(); -var - ResultCode: Integer; -begin - if not ExecStep( - ExpandConstant('{app}\{#Alias}.exe'), - '--register-installed-versions', - 'NVM4107', - 'Registering installed Node.js versions exceeded its time limit.', - WalkTimeoutSec(CountInstalledNodeVersions(GetInstallRoot(''))), - True, - False, - ResultCode - ) then - begin - Log('Unable to start installed-version registration for Windows Apps.'); - Exit; - end; - - if ResultCode <> 0 then - Log('Installed-version registration returned exit code ' + IntToStr(ResultCode) + '.'); -end; - -procedure SignInstalledVersionScripts(); -var - ResultCode: Integer; -begin - { 2.0.0 upgrades have Node installs and no npm-cli.js script-trust rows. - reshim.exe swallows signer failures; call nvm directly and keep the exit code. } - if not ExecStep( - ExpandConstant('{app}\{#Alias}.exe'), - '--sign-installed-versions', - 'NVM4107', - 'Signing installed Node.js versions exceeded its time limit.', - WalkTimeoutSec(CountInstalledNodeVersions(GetInstallRoot(''))), - True, - False, - ResultCode - ) then - begin - AppendInstallLogWarn('SignInstalledVersionScripts: unable to start nvm.exe --sign-installed-versions'); - Exit; - end; - - if ResultCode <> 0 then - AppendInstallLogWarn('SignInstalledVersionScripts: nvm.exe --sign-installed-versions returned exit code ' + IntToStr(ResultCode)); -end; - -procedure RunForceReshimAtEnd(); -var - ResultCode: Integer; -begin - if not ExecStep( - ExpandConstant('{app}\utils\reshim.exe'), - '--force', - 'NVM4113', - 'Rebuilding shims exceeded its time limit.', - WalkTimeoutSec(CountInstalledNodeVersions(GetInstallRoot(''))), - True, - False, - ResultCode - ) then - begin - ShimFinalizeIncomplete := True; - AppendInstallLogWarn('RunForceReshimAtEnd: unable to start reshim.exe --force'); - Exit; - end; - - if ResultCode <> 0 then - begin - ShimFinalizeIncomplete := True; - AppendInstallLogWarn('RunForceReshimAtEnd: reshim.exe --force returned exit code ' + IntToStr(ResultCode)); - end; -end; - -procedure WarnIfShimFinalizeIncomplete(); -var - MessageText: String; -begin - if not ShimFinalizeIncomplete then - Exit; - - AppendInstallLogWarn( - 'Shim finalize incomplete (locked/hung Node process). User must close Node apps and run: nvm reshim' - ); - - if WizardSilent then - Exit; - - MessageText := - 'NVM could not finish rebuilding shims because a Node.js process was locked or hung.' + #13#10 + #13#10 + - 'After this installer closes:' + #13#10 + - '1. Close all terminals and apps using Node.js / npm / npx' + #13#10 + - '2. Open a new terminal' + #13#10 + - '3. Run: nvm reshim' + #13#10 + #13#10 + - 'Until you run that command, npm/node may fail or use stale shims.'; - - MsgBox(MessageText, mbInformation, MB_OK); -end; - -procedure EnsureShimHardlinkForPrewarm(const ShimBaseName: String); -var - ResultCode: Integer; - ShimPath: String; - ProxyPath: String; - DataRoot: String; - DataShim: String; - CommandLine: String; -begin - DataRoot := NormalizePath(GetDataRoot('')); - DataShim := AddBackslash(DataRoot) + '.shim'; - ShimPath := AddBackslash(DataShim) + ShimBaseName + '.exe'; - - { Prefer DataRoot proxy when present (required for cross-volume hardlinks). } - ProxyPath := AddBackslash(DataRoot) + 'proxy.exe'; - if not FileExists(ProxyPath) then - ProxyPath := ExpandConstant('{app}\utils\proxy.exe'); - - if FileExists(ShimPath) then - Exit; - - if not FileExists(ProxyPath) then - begin - AppendInstallLogWarn('EnsureShimHardlinkForPrewarm: missing proxy.exe: ' + ProxyPath); - Exit; - end; - - if not DirExists(DataShim) then - begin - AppendInstallLogWarn('EnsureShimHardlinkForPrewarm: missing .shim directory: ' + DataShim); - Exit; - end; - - CommandLine := - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ' + - '"$shim = ''' + ShimPath + '''; ' + - '$proxy = ''' + ProxyPath + '''; ' + - 'if (-not (Test-Path -LiteralPath $shim)) { New-Item -ItemType HardLink -Path $shim -Target $proxy -ErrorAction Stop | Out-Null }"'; - - if not Exec(ExpandConstant('{cmd}'), CommandLine, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - AppendInstallLogWarn('EnsureShimHardlinkForPrewarm: failed to create hardlink for ' + ShimBaseName); - Exit; - end; - - if ResultCode <> 0 then - AppendInstallLogWarn('EnsureShimHardlinkForPrewarm: hardlink creation returned exit code ' + IntToStr(ResultCode) + ' for ' + ShimBaseName); -end; - -procedure TryPrewarmShim(const ShimBaseName: String); -var - ResultCode: Integer; - ShimPath: String; - CommandLine: String; -begin - ResultCode := -1; - ShimPath := AddBackslash(GetDataRoot('')) + '.shim\' + ShimBaseName + '.exe'; - if not FileExists(ShimPath) then - begin - AppendInstallLogWarn('TryPrewarmShim: missing ' + ShimPath); - Exit; - end; - - { Timeout: hung npm/node (see #1390) must not block setup forever. - Redirect all stdio to temp files β€” Hidden Start-Process without redirects - still attaches conhost; inherited console I/O can hang forever under Inno. } - CommandLine := - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ' + - '"$shim = ''' + EscapeSingleQuotedPowerShellString(ShimPath) + '''; ' + - '$base = Join-Path $env:TEMP (''nvm-prewarm-'' + [guid]::NewGuid().ToString()); ' + - '$out = $base + ''.out''; $err = $base + ''.err''; $in = $base + ''.in''; ' + - 'New-Item -LiteralPath $in -ItemType File -Force | Out-Null; ' + - 'try { ' + - ' $p = Start-Process -FilePath $shim -ArgumentList ''--version'' -WindowStyle Hidden -PassThru ' + - ' -RedirectStandardInput $in -RedirectStandardOutput $out -RedirectStandardError $err; ' + - ' if (-not $p.WaitForExit(15000)) { ' + - ' Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue; ' + - ' exit 124 ' + - ' } else { exit $p.ExitCode } ' + - '} finally { ' + - ' Remove-Item -LiteralPath $out,$err,$in -Force -ErrorAction SilentlyContinue ' + - '}"'; - - if Exec(ExpandConstant('{cmd}'), CommandLine, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - if ResultCode = 124 then - begin - ShimFinalizeIncomplete := True; - AppendInstallLogWarn( - 'TryPrewarmShim: ' + ShimBaseName + - '.exe timed out after 15s (process locked/hung) β€” user must run nvm reshim after install' - ); - end - else - AppendInstallLog('TryPrewarmShim: ' + ShimBaseName + '.exe exit code ' + IntToStr(ResultCode)); - end - else - AppendInstallLog('TryPrewarmShim: unable to start ' + ShimBaseName + '.exe (ignored)'); -end; - -procedure ForceCloseNvmProcessesForInstall(); -var - ScriptFile: String; - AppRoot: String; - DataRoot: String; - ResultCode: Integer; -begin - AppRoot := RemoveBackslashUnlessRoot(ExpandConstant('{app}')); - DataRoot := RemoveBackslashUnlessRoot(GetDataRoot('')); - - ScriptFile := ExpandConstant('{tmp}\nvm-install-force-close.ps1'); - SaveStringToFile( - ScriptFile, - BuildNvmProcessManagementScript( - AppRoot, - DataRoot, - '', - 'stop', - ExpandConstant('{srcexe}') - ), - False - ); - - if ExecStep( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - 'NVM4114', - 'Closing Node.js processes exceeded 3 minutes.', - 180, - True, - False, - ResultCode - ) then - AppendInstallLog('ForceCloseNvmProcessesForInstall: exit=' + IntToStr(ResultCode)) - else - AppendInstallLogWarn('ForceCloseNvmProcessesForInstall: failed to start'); - - DeleteFile(ScriptFile); -end; - -function ScanBlockingInstallProcesses(var ProcessList: String): Boolean; -var - ScriptFile: String; - OutputFile: String; - RawOutput: AnsiString; - RawText: String; - ResultCode: Integer; - LineStart, LineEnd, LineLength: Integer; - Line: String; - FormattedLine: String; - AppRoot: String; - DataRoot: String; -begin - Result := False; - ProcessList := ''; - - AppRoot := RemoveBackslashUnlessRoot(ExpandConstant('{app}')); - DataRoot := RemoveBackslashUnlessRoot(GetDataRoot('')); - - OutputFile := ExpandConstant('{tmp}\nvm-install-blockers.txt'); - DeleteFile(OutputFile); - - ScriptFile := ExpandConstant('{tmp}\nvm-install-list-processes.ps1'); - SaveStringToFile( - ScriptFile, - BuildNvmProcessManagementScript( - AppRoot, - DataRoot, - OutputFile, - 'list', - ExpandConstant('{srcexe}') - ), - False - ); - - if Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - begin - if LoadStringFromFile(OutputFile, RawOutput) then - begin - RawText := String(RawOutput); - LineStart := 1; - while LineStart <= Length(RawText) do - begin - LineEnd := LineStart; - while (LineEnd <= Length(RawText)) and (RawText[LineEnd] <> #10) and (RawText[LineEnd] <> #13) do - LineEnd := LineEnd + 1; - - LineLength := LineEnd - LineStart; - if LineLength > 0 then - begin - Line := Trim(Copy(RawText, LineStart, LineLength)); - FormattedLine := FormatBlockingProcessLine(Line); - if FormattedLine <> '' then - begin - if ProcessList <> '' then - ProcessList := ProcessList + #13#10; - ProcessList := ProcessList + FormattedLine; - end; - end; - - LineStart := LineEnd + 1; - if (LineStart <= Length(RawText)) and (RawText[LineStart] = #10) then - LineStart := LineStart + 1; - end; - end; - end - else - AppendInstallLogWarn('ScanBlockingInstallProcesses: scan script failed to start'); - - DeleteFile(ScriptFile); - DeleteFile(OutputFile); - - ProcessList := Trim(ProcessList); - Result := ProcessList <> ''; -end; - -{ Scrollable process prompt. Buttons stay on-screen even with long lists. - Returns IDYES / IDNO / IDCANCEL (Cancel = "check again"). - Form X / Escape returns IDNO so the user is never trapped in a re-prompt loop. } -function PromptBlockingProcessList( - const DialogTitle, IntroText, ProcessList, Footnote: String; - const YesCaption, NoCaption, CheckAgainCaption: String -): Integer; -var - Form: TSetupForm; - IntroLabel: TNewStaticText; - ListMemo: TNewMemo; - FootLabel: TNewStaticText; - YesButton: TNewButton; - NoButton: TNewButton; - CheckAgainButton: TNewButton; - ButtonTop: Integer; - ButtonWidth: Integer; - Gap: Integer; - ContentWidth: Integer; - ModalResult: Integer; -begin - Form := CreateCustomForm(ScaleX(520), ScaleY(420), False, False); - try - Form.Caption := DialogTitle; - - ContentWidth := Form.ClientWidth - ScaleX(24); - - IntroLabel := TNewStaticText.Create(Form); - IntroLabel.Parent := Form; - IntroLabel.Left := ScaleX(12); - IntroLabel.Top := ScaleY(12); - IntroLabel.Width := ContentWidth; - IntroLabel.AutoSize := False; - IntroLabel.WordWrap := True; - IntroLabel.Caption := IntroText; - IntroLabel.Height := ScaleY(48); - - FootLabel := TNewStaticText.Create(Form); - FootLabel.Parent := Form; - FootLabel.Left := ScaleX(12); - FootLabel.Width := ContentWidth; - FootLabel.AutoSize := False; - FootLabel.WordWrap := True; - FootLabel.Caption := Footnote; - FootLabel.Height := ScaleY(52); - FootLabel.Top := Form.ClientHeight - ScaleY(96); - - ButtonWidth := ScaleX(120); - Gap := ScaleX(8); - ButtonTop := Form.ClientHeight - ScaleY(40); - - YesButton := TNewButton.Create(Form); - YesButton.Parent := Form; - YesButton.Caption := YesCaption; - YesButton.ModalResult := mrYes; - YesButton.Default := True; - YesButton.Width := ButtonWidth; - YesButton.Height := ScaleY(24); - YesButton.Left := Form.ClientWidth - ScaleX(12) - (ButtonWidth * 3) - (Gap * 2); - YesButton.Top := ButtonTop; - - NoButton := TNewButton.Create(Form); - NoButton.Parent := Form; - NoButton.Caption := NoCaption; - NoButton.ModalResult := mrNo; - NoButton.Cancel := True; { Escape / title-bar X β†’ No (escape hatch) } - NoButton.Width := ButtonWidth; - NoButton.Height := ScaleY(24); - NoButton.Left := YesButton.Left + ButtonWidth + Gap; - NoButton.Top := ButtonTop; - - CheckAgainButton := TNewButton.Create(Form); - CheckAgainButton.Parent := Form; - CheckAgainButton.Caption := CheckAgainCaption; - CheckAgainButton.ModalResult := mrRetry; - CheckAgainButton.Width := ButtonWidth; - CheckAgainButton.Height := ScaleY(24); - CheckAgainButton.Left := NoButton.Left + ButtonWidth + Gap; - CheckAgainButton.Top := ButtonTop; - - ListMemo := TNewMemo.Create(Form); - ListMemo.Parent := Form; - ListMemo.Left := ScaleX(12); - ListMemo.Top := IntroLabel.Top + IntroLabel.Height + ScaleY(8); - ListMemo.Width := ContentWidth; - ListMemo.Height := FootLabel.Top - ListMemo.Top - ScaleY(8); - ListMemo.ReadOnly := True; - ListMemo.ScrollBars := ssVertical; - ListMemo.WantReturns := True; - ListMemo.Text := ProcessList; - - Form.FlipAndCenterIfNeeded(False, WizardForm, False); - ModalResult := Form.ShowModal(); - finally - Form.Free(); - end; - - case ModalResult of - mrYes: - Result := IDYES; - mrRetry: - Result := IDCANCEL; { keep prior "check again" loop semantics } - else - Result := IDNO; { mrNo, mrCancel, X, Escape } - end; -end; - -{ Returns True when install should continue (possibly after closing blockers). } -function ConfirmAndCloseBlockingInstallProcesses(): Boolean; -var - ProcessList: String; - Response: Integer; -begin - Result := True; - - if WizardSilent then - begin - if ScanBlockingInstallProcesses(ProcessList) then - begin - AppendInstallLogWarn('Silent install: closing NVM/Node processes under install roots:' + #13#10 + ProcessList); - ForceCloseNvmProcessesForInstall(); - end; - Exit; - end; - - while True do - begin - if not ScanBlockingInstallProcesses(ProcessList) then - Break; - - Response := PromptBlockingProcessList( - '{#Name}', - 'These Node.js processes are still running and can block shim updates:', - ProcessList, - 'Yes = close them and continue' + #13#10 + - 'No = continue without closing (shim rebuild may time out; run nvm reshim later)' + #13#10 + - 'Check again = rescan after you close apps manually', - '&Yes', - '&No', - 'Check &again' - ); - - if Response = IDYES then - begin - ForceCloseNvmProcessesForInstall(); - Continue; - end; - - if Response = IDNO then - begin - AppendInstallLogWarn('User declined to close blocking processes; continuing'); - Break; - end; - end; -end; - -procedure PrewarmNpmAndNpxShims(); -begin - { node + common package managers; missing hardlinks/skips are ignored. - node.exe is the program shim (not proxy) β€” only prewarm if reshim already created it. } - TryPrewarmShim('node'); - EnsureShimHardlinkForPrewarm('npm'); - EnsureShimHardlinkForPrewarm('npx'); - EnsureShimHardlinkForPrewarm('yarn'); - EnsureShimHardlinkForPrewarm('pnpm'); - TryPrewarmShim('npm'); - TryPrewarmShim('npx'); - TryPrewarmShim('yarn'); - TryPrewarmShim('pnpm'); -end; - -procedure UpdateFinalizingProgress( - ProgressPage: TOutputProgressWizardPage; - const DetailText: String; - CurrentStep, TotalSteps: Integer -); -begin - if ProgressPage = nil then - Exit; - - ProgressPage.SetText('Finalizing installation', DetailText); - ProgressPage.SetProgress(CurrentStep, TotalSteps); -end; - -procedure ConfigureAUMIDForNotifications(); -begin - { Always refresh AUMID values so upgrades fix stale notification icon settings } - RegWriteStringValue(HKCU, 'Software\Classes\AppUserModelId\AuthorSoftware.NVMWindows', 'DisplayName', '{#Name}'); - RegWriteStringValue(HKCU, 'Software\Classes\AppUserModelId\AuthorSoftware.NVMWindows', 'IconUri', ExpandConstant('{app}\.icons\{#Alias}.ico')); -end; - -procedure ConfigureUninstallDisplayIcon(); -var - UninstallKey: String; - CoreVersion: String; - Position: Integer; - MajorVer: Integer; - MinorVer: Integer; -begin - { Always refresh ARP metadata so Installed apps shows logo + version after upgrade. - Inno can leave DisplayVersion stale when UsePreviousAppDir=no reuses the same _is1 key. } - UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#AppId}_is1'; - RegWriteStringValue( - HKCU, - UninstallKey, - 'DisplayIcon', - ExpandConstant('{app}\.icons\{#Alias}.ico') - ); - RegWriteStringValue(HKCU, UninstallKey, 'DisplayName', '{#Name}'); - RegWriteStringValue(HKCU, UninstallKey, 'DisplayVersion', '{#Version}'); - - CoreVersion := GetCoreVersion('{#Version}'); - Position := 1; - MajorVer := NextVersionPart(CoreVersion, Position); - MinorVer := NextVersionPart(CoreVersion, Position); - if MajorVer < 0 then - MajorVer := 0; - if MinorVer < 0 then - MinorVer := 0; - RegWriteDWordValue(HKCU, UninstallKey, 'VersionMajor', MajorVer); - RegWriteDWordValue(HKCU, UninstallKey, 'VersionMinor', MinorVer); - RegWriteDWordValue(HKCU, UninstallKey, 'MajorVersion', MajorVer); - RegWriteDWordValue(HKCU, UninstallKey, 'MinorVersion', MinorVer); - - { Keep preference Version in sync for detection / migration prompts. } - RegWriteStringValue(HKCU, '{#RegistryKey}', 'Version', '{#Version}'); -end; - -procedure CurStepChanged(CurStep: TSetupStep); -var - ResultCode: Integer; - ElevatedExecOk: Boolean; - FinalizingPage: TOutputProgressWizardPage; - FinalizingStep: Integer; - FinalizingTotal: Integer; -begin - if CurStep = ssDone then - begin - InstallCompleted := True; - Exit; - end; - - if CurStep <> ssPostInstall then - Exit; - - FinalizingTotal := 7; - if RemoveLegacyTasks then - FinalizingTotal := FinalizingTotal + 1; - FinalizingTotal := FinalizingTotal + 1; { silent/ACL storage policy check } - FinalizingTotal := FinalizingTotal + 1; { for AUMID configuration } - FinalizingTotal := FinalizingTotal + 1; { close blocking Node/NVM processes } - FinalizingTotal := FinalizingTotal + 1; { for forced reshim at final step } - FinalizingTotal := FinalizingTotal + 1; { for runtime ACL repair } - if OfficialNodeDetected and (not DeferOfficialNodeOnV2Upgrade) then - FinalizingTotal := FinalizingTotal + 1; - - FinalizingStep := 0; - FinalizingPage := CreateOutputProgressPage( - 'Finalizing installation', - 'Completing migration and Windows integration.' - ); - FinalizingPage.SetProgress(0, FinalizingTotal); - FinalizingPage.Show; - ResetInstallLog(); - AppendInstallLog('CurStepChanged: ssPostInstall start'); - AppendInstallLog('CurStepChanged: app=' + ExpandConstant('{app}')); - AppendInstallLog('CurStepChanged: install root=' + GetInstallRoot('')); - AppendInstallLog('CurStepChanged: data root=' + GetDataRoot('')); - AppendInstallLog('CurStepChanged: LegacyInstallDir=' + LegacyInstallDir); - AppendInstallLog('CurStepChanged: LegacySettingsRoot=' + LegacySettingsRoot); - AppendInstallLog('CurStepChanged: LegacySettingsPath=' + LegacySettingsPath); - AppendInstallLog('CurStepChanged: PreviousInstallRoot=' + PreviousInstallRoot); - AppendInstallLog('CurStepChanged: RemoveLegacyTasks=' + IntToStr(Integer(RemoveLegacyTasks))); - AppendInstallLog('CurStepChanged: IsPreV2Upgrade=' + IntToStr(Integer(IsPreV2Upgrade))); - ResetLegacyCleanupWarnings(); - - try - if RemoveLegacyTasks then - begin - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Removing legacy scheduled tasks...', FinalizingStep, FinalizingTotal); - RemoveLegacyScheduledTasks(); - if InstallerHardFailed then - Exit; - end; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Checking Node.js storage permissions...', FinalizingStep, FinalizingTotal); - ApplySilentInstallRootPolicy(); - AppendInstallLog('CurStepChanged: install root after ACL policy=' + GetInstallRoot('')); - - RemoveLegacySymlinkPathIfNeeded(); - if InstallerHardFailed then - Exit; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Migrating installed Node.js versions...', FinalizingStep, FinalizingTotal); - MigrateNodeStorageIfNeeded(); - if InstallerHardFailed then - Exit; - if OfficialNodeDetected and (not DeferOfficialNodeOnV2Upgrade) then - begin - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Applying official Node.js choice...', FinalizingStep, FinalizingTotal); - ApplyOfficialNodeChoice(); - if InstallerHardFailed then - Exit; - end; - if MigrationPerformed and HasSkippedSymlinks() then - MsgBox( - 'Some migrated links could not be recreated automatically.' + #13#10 + #13#10 + - 'Review the mklink commands in:' + #13#10 + - SkippedSymlinkLogPath + #13#10 + #13#10 + - 'If you still need them, run those commands manually after setup.', - mbInformation, - MB_OK - ); - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Registering installed Node.js versions in Windows Apps...', FinalizingStep, FinalizingTotal); - RegisterInstalledVersionsInWindowsApps(); - if InstallerHardFailed then - Exit; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Creating background sync scheduled task...', FinalizingStep, FinalizingTotal); - CreateSyncScheduledTask(); - if InstallerHardFailed then - Exit; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Configuring internal Node.js shim junction...', FinalizingStep, FinalizingTotal); - EnsureDataRootRuntimeLayout(); - if not EnsureShimModeNodePathJunction() then - MsgBox( - 'NVM for Windows could not create the internal .nodejs junction required for shim mode.' + #13#10 + #13#10 + - 'Shim mode may not work correctly until the junction is repaired.', - mbInformation, - MB_OK - ); - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Updating environment variables and PATH priority...', FinalizingStep, FinalizingTotal); - - if IsPreV2Upgrade then - begin - { For v1 upgrades: one UAC prompt covers all privileged cleanup + event log registration. } - { The helper clears NVM_HOME/NVM_SYMLINK from user+system env and PATH, removes the old } - { uninstall keys and install directory, then runs nvm.exe --register-eventlog internally. } - UpdateFinalizingProgress(FinalizingPage, 'Running v1 migration cleanup (UAC prompt may appear)...', FinalizingStep, FinalizingTotal); - RunUpgraderHelper(); - { Re-establish the new v2 NVM_HOME in user env and user PATH after the helper cleared them. } - EnsureNvmPathPriority(); - end - else - begin - EnsureNvmPathPriority(); - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Registering Windows Event Log source (UAC prompt may appear)...', FinalizingStep, FinalizingTotal); - - ElevatedExecOk := ExecStep( - ExpandConstant('{app}\{#Alias}.exe'), - '--register-eventlog', - 'NVM4111', - 'Registering the Windows event log exceeded 5 minutes.', - 300, - True, - True, - ResultCode - ); - - { Event source registration is best-effort. Non-admin / declined UAC is expected; - continue install silently β€” logs still work as "Unknown" source in Event Viewer. } - if not ElevatedExecOk then - begin - if ResultCode = 1223 then - AppendInstallLogWarn('Event Log source registration skipped: UAC prompt canceled') - else - AppendInstallLogWarn( - 'Event Log source registration skipped: elevation failed (system error ' + IntToStr(ResultCode) + ')' - ); - end - else if ResultCode <> 0 then - AppendInstallLogWarn( - 'Event Log source registration returned exit code ' + IntToStr(ResultCode) - ) - else - AppendInstallLog('Event Log source registered successfully'); - end; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Hardening install and Node.js version ACLs...', FinalizingStep, FinalizingTotal); - RepairRuntimeAclsPostInstall(); - if InstallerHardFailed then - Exit; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Configuring notification center integration...', FinalizingStep, FinalizingTotal); - ConfigureAUMIDForNotifications(); - ConfigureUninstallDisplayIcon(); - NotifySilentStorageMigration(); - NotifyOfficialNodeLeftInPlace(); - - if RuntimeACLDegraded then - AppendInstallLogWarn( - 'Runtime ACL state is degraded. Run `nvm doctor --autofix` (elevation may be prompted) before using npm.' - ); - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Closing Node.js processes that lock shims...', FinalizingStep, FinalizingTotal); - ConfirmAndCloseBlockingInstallProcesses(); - if InstallerHardFailed then - Exit; - - FinalizingStep := FinalizingStep + 1; - UpdateFinalizingProgress(FinalizingPage, 'Rebuilding and prewarming module shims...', FinalizingStep, FinalizingTotal); - RunForceReshimAtEnd(); - if InstallerHardFailed then - Exit; - SignInstalledVersionScripts(); - PrewarmNpmAndNpxShims(); - WarnIfShimFinalizeIncomplete(); - WarnIfOfficialNodeIncomplete(); - - if ShouldRunSubscriptionCommand() then - begin - if not Exec( - ExpandConstant('{cmd}'), - '/C start "" /B "' + ExpandConstant('{app}\{#Alias}.exe') + '" subscribe "' + EscapeDoubleQuotedCommandArgument(GetSubscriptionEmail()) + '"', - '', - SW_HIDE, - ewNoWait, - ResultCode - ) then - begin - AppendInstallLogWarn('Failed to launch nvm subscribe command (system error ' + IntToStr(ResultCode) + ')'); - end; - end - else - AppendInstallLog('Subscription command skipped: no email provided'); - finally - FlushInstallLog(); - FinalizingPage.Hide; - end; -end; -procedure DeinitializeSetup(); -var - RollbackTotal, RollbackCount: Integer; -begin - AppendInstallLog('DeinitializeSetup: begin'); - if InstallerHardFailed then - begin - if (not WizardSilent) then - ShowInstallerFailureDialog(); - ScheduleInstallerRollback(); - FlushInstallLog(); - ExitProcess(1); - end; - - if InstallCompleted or not MigrationPerformed then - begin - AppendInstallLog('DeinitializeSetup: no rollback needed'); - Exit; - end; - - if not DirExists(MigrationDest) then - begin - AppendInstallLogWarn('DeinitializeSetup: migration destination missing, skipping rollback'); - Exit; - end; - - if not ForceDirectories(MigrationSource) then - begin - AppendInstallLogWarn('DeinitializeSetup: could not recreate migration source for rollback'); - Exit; - end; - - RollbackTotal := 0; - CountFilesInTree(MigrationDest, RollbackTotal); - RollbackCount := 0; - - SuppressStepFailure := True; - if CopyTreeWithProgress(MigrationDest, MigrationSource, nil, RollbackCount, RollbackTotal, '') then - begin - AppendInstallLog('DeinitializeSetup: rollback copy succeeded, deleting migration destination'); - DelTree(MigrationDest, True, True, True); - MigrationPerformed := False; - end; - FlushInstallLog(); -end; - -procedure ResetUninstallLeftovers(); -begin - UninstallLeftovers := ''; - UninstallDelayedCleanupScheduled := False; -end; - -function BuildNvmProcessManagementScript( - const AppRoot, InstallRoot, OutputFile, Mode, ExcludeProcessPath: String -): String; -var - AppEscaped, InstallEscaped, OutputEscaped, ModeEscaped, ExcludeEscaped: String; - ListBranch: String; -begin - AppEscaped := EscapeSingleQuotedPowerShellString(AppRoot); - InstallEscaped := EscapeSingleQuotedPowerShellString(InstallRoot); - OutputEscaped := EscapeSingleQuotedPowerShellString(OutputFile); - ModeEscaped := EscapeSingleQuotedPowerShellString(Mode); - ExcludeEscaped := EscapeSingleQuotedPowerShellString(ExcludeProcessPath); - - if CompareText(Mode, 'list') = 0 then - ListBranch := - ' $line = $Name + ''|'' + $ProcId + ''|'' + $Detail' + #13#10 + - ' $utf8NoBom = New-Object System.Text.UTF8Encoding $false' + #13#10 + - ' [System.IO.File]::AppendAllText($outFile, $line + [Environment]::NewLine, $utf8NoBom)' + #13#10 - else - ListBranch := - ' Stop-Process -Id ([int]$ProcId) -Force -ErrorAction SilentlyContinue' + #13#10; - - Result := - '$mode = ''' + ModeEscaped + '''' + #13#10 + - '$outFile = ''' + OutputEscaped + '''' + #13#10 + - '$excludePath = ''' + ExcludeEscaped + '''' + #13#10 + - '$seen = @{}' + #13#10 + - 'function Test-ExcludedNvmProcess([string]$Name, [string]$Detail) {' + #13#10 + - ' if ($Name -match ''^(?i)unins\d*$'') { return $true }' + #13#10 + - ' if ($excludePath -and $Detail -and ($Detail -ieq $excludePath)) { return $true }' + #13#10 + - ' return $false' + #13#10 + - '}' + #13#10 + - 'function Register-NvmRootProcessMatch([string]$ProcId, [string]$Name, [string]$Detail) {' + #13#10 + - ' if (Test-ExcludedNvmProcess $Name $Detail) { return }' + #13#10 + - ' if ($seen.ContainsKey($ProcId)) { return }' + #13#10 + - ' $seen[$ProcId] = $true' + #13#10 + - ' if ($mode -eq ''list'') {' + #13#10 + - ListBranch + - ' } else {' + #13#10 + - ' Stop-Process -Id ([int]$ProcId) -Force -ErrorAction SilentlyContinue' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - 'function Invoke-NvmRootProcesses([string]$Root) {' + #13#10 + - ' if ([string]::IsNullOrWhiteSpace($Root)) { return }' + #13#10 + - ' if (-not (Test-Path -LiteralPath $Root)) { return }' + #13#10 + - ' $rootNorm = $Root.TrimEnd(''\'')' + #13#10 + - ' Get-Process -ErrorAction SilentlyContinue | ForEach-Object {' + #13#10 + - ' try {' + #13#10 + - ' $path = $_.Path' + #13#10 + - ' if ($null -ne $path -and $path.StartsWith($rootNorm, [StringComparison]::OrdinalIgnoreCase)) {' + #13#10 + - ' Register-NvmRootProcessMatch ([string]$_.Id) $_.ProcessName $path' + #13#10 + - ' }' + #13#10 + - ' } catch {}' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - '$appRoot = ''' + AppEscaped + '''' + #13#10 + - '$installRoot = ''' + InstallEscaped + '''' + #13#10 + - 'if ($mode -eq ''list'' -and (Test-Path -LiteralPath $outFile)) { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue }' + #13#10 + - 'if ($mode -eq ''list'') { New-Item -Path $outFile -ItemType File -Force | Out-Null }' + #13#10 + - // Only match image paths under NVM roots. Name-wide node/npm/yarn scans - // pull in unmanaged installs (system Node, Volta, fnm, IDE embeds) as noise. - 'Invoke-NvmRootProcesses $appRoot' + #13#10 + - 'if ($installRoot -and ($installRoot -ne $appRoot)) { Invoke-NvmRootProcesses $installRoot }' + #13#10 + - 'if ($mode -ne ''list'') { Start-Sleep -Seconds 2 }'; -end; - -procedure AppendUninstallLeftover(const Line: String); -var - Trimmed: String; -begin - Trimmed := Trim(Line); - if Trimmed = '' then - Exit; - - if Pos(Trimmed, UninstallLeftovers) > 0 then - Exit; - - if UninstallLeftovers <> '' then - UninstallLeftovers := UninstallLeftovers + #13#10; - - UninstallLeftovers := UninstallLeftovers + Trimmed; -end; - -procedure NotePathLeftoverIfExists(const Description, PathValue: String); -var - TrimmedPath: String; -begin - TrimmedPath := Trim(PathValue); - if TrimmedPath = '' then - Exit; - - if FileExists(TrimmedPath) or DirExists(TrimmedPath) then - AppendUninstallLeftover(Description + ': ' + TrimmedPath); -end; - -function SyncTaskStillExists(): Boolean; -var - ResultCode: Integer; -begin - Result := Exec( - ExpandConstant('{cmd}'), - '/C schtasks /Query /TN "' + SyncTaskName + '" >NUL 2>&1', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) and (ResultCode = 0); -end; - -procedure RemoveSyncTaskOnUninstall(); -var - ResultCode: Integer; -begin - if not SyncTaskStillExists() then - Exit; - - if Exec( - ExpandConstant('{cmd}'), - '/C schtasks /Delete /TN "' + SyncTaskName + '" /F', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - begin - if ResultCode <> 0 then - Log('Scheduled task could not be removed during uninstall: ' + SyncTaskName + ' (exit code ' + IntToStr(ResultCode) + ').'); - end - else - Log('Scheduled task removal command failed during uninstall: ' + SyncTaskName + '.'); -end; - -function BuildStopProcessesUnderRootsPowerShell( - const AppRoot, InstallRoot: String -): String; -begin - Result := BuildNvmProcessManagementScript( - AppRoot, InstallRoot, '', 'stop', UninstallSelfExe); -end; - -function BuildListProcessesUnderRootsPowerShell( - const AppRoot, InstallRoot, OutputFile: String -): String; -begin - Result := BuildNvmProcessManagementScript( - AppRoot, InstallRoot, OutputFile, 'list', UninstallSelfExe); -end; - -function StripUtf8Bom(const Value: String): String; -begin - Result := Value; - if (Length(Result) >= 3) and (Copy(Result, 1, 3) = #$EF#$BB#$BF) then - Delete(Result, 1, 3); -end; - -function FormatBlockingProcessLine(const Line: String): String; -var - Pipe1, Pipe2: Integer; - NamePart, PidPart, PathPart: String; -begin - Result := ''; - Pipe1 := Pos('|', Line); - if Pipe1 = 0 then - Exit; - - Pipe2 := Pos('|', Copy(Line, Pipe1 + 1, MaxInt)); - if Pipe2 = 0 then - Exit; - - NamePart := StripUtf8Bom(Trim(Copy(Line, 1, Pipe1 - 1))); - PidPart := Trim(Copy(Line, Pipe1 + 1, Pipe2 - 1)); - PathPart := Trim(Copy(Line, Pipe1 + Pipe2 + 1, MaxInt)); - - if (NamePart = '') or (PidPart = '') then - Exit; - - if CompareText(NamePart, 'unins000') = 0 then - Exit; - - if (UninstallSelfExe <> '') and (CompareText(PathPart, UninstallSelfExe) = 0) then - Exit; - - Result := '- ' + NamePart + ' (PID ' + PidPart + ')' + #13#10 + ' ' + PathPart; -end; - -function ScanBlockingUninstallProcesses( - const AppRoot, InstallRoot: String; - var ProcessList: String -): Boolean; -var - ScriptFile: String; - OutputFile: String; - RawOutput: AnsiString; - RawText: String; - ResultCode: Integer; - LineStart, LineEnd, LineLength: Integer; - Line: String; - FormattedLine: String; -begin - Result := False; - ProcessList := ''; - - OutputFile := ExpandConstant('{tmp}\nvm-uninstall-blockers.txt'); - DeleteFile(OutputFile); - - ScriptFile := ExpandConstant('{tmp}\nvm-uninstall-list-processes.ps1'); - SaveStringToFile( - ScriptFile, - BuildListProcessesUnderRootsPowerShell(AppRoot, InstallRoot, OutputFile), - False - ); - - if Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - begin - if LoadStringFromFile(OutputFile, RawOutput) then - begin - RawText := String(RawOutput); - LineStart := 1; - while LineStart <= Length(RawText) do - begin - LineEnd := LineStart; - while (LineEnd <= Length(RawText)) and (RawText[LineEnd] <> #10) and (RawText[LineEnd] <> #13) do - LineEnd := LineEnd + 1; - - LineLength := LineEnd - LineStart; - if LineLength > 0 then - begin - Line := Trim(Copy(RawText, LineStart, LineLength)); - FormattedLine := FormatBlockingProcessLine(Line); - if FormattedLine <> '' then - begin - if ProcessList <> '' then - ProcessList := ProcessList + #13#10; - ProcessList := ProcessList + FormattedLine; - end; - end; - - LineStart := LineEnd + 1; - if (LineStart <= Length(RawText)) and (RawText[LineStart] = #10) then - LineStart := LineStart + 1; - end; - end; - end - else - Log('Blocking-process scan script failed to start.'); - - DeleteFile(ScriptFile); - DeleteFile(OutputFile); - - ProcessList := Trim(ProcessList); - Result := ProcessList <> ''; -end; - -function ConfirmUninstallBlockingProcesses(): Boolean; -var - AppRoot: String; - InstallRoot: String; - DataRoot: String; - ProcessList: String; - Response: Integer; -begin - Result := True; - - AppRoot := RemoveBackslashUnlessRoot(Trim(UninstallAppRoot)); - if AppRoot = '' then - AppRoot := RemoveBackslashUnlessRoot(ExpandConstant('{app}')); - - InstallRoot := RemoveBackslashUnlessRoot(Trim(UninstallInstallRoot)); - if InstallRoot = '' then - InstallRoot := RemoveBackslashUnlessRoot(GetInstallRootForUninstall('')); - - // DataRoot owns .shim; InstallRoot alone misses it when storage is split. - DataRoot := RemoveBackslashUnlessRoot(ExtractFileDir(InstallRoot)); - if DataRoot = '' then - DataRoot := InstallRoot; - - while True do - begin - if not ScanBlockingUninstallProcesses(AppRoot, DataRoot, ProcessList) then - Break; - - Response := PromptBlockingProcessList( - '{#Name} Uninstall', - 'Close these NVM processes before uninstall can continue:', - ProcessList, - 'Yes = close them and continue' + #13#10 + - 'No = cancel uninstall' + #13#10 + - 'Check again = rescan after you close apps manually', - '&Yes', - '&No', - 'Check &again' - ); - - if Response = IDNO then - begin - Result := False; - Exit; - end; - - if Response = IDYES then - begin - ForceCloseNvmProcessesOnUninstall(); - Break; - end; - end; -end; - -procedure ForceCloseNvmProcessesOnUninstall(); -var - ScriptFile: String; - AppRoot: String; - InstallRoot: String; - DataRoot: String; - ResultCode: Integer; -begin - AppRoot := Trim(UninstallAppRoot); - if AppRoot = '' then - AppRoot := ExpandConstant('{app}'); - AppRoot := RemoveBackslashUnlessRoot(AppRoot); - - InstallRoot := Trim(UninstallInstallRoot); - if InstallRoot = '' then - InstallRoot := GetInstallRootForUninstall(''); - InstallRoot := RemoveBackslashUnlessRoot(InstallRoot); - - DataRoot := RemoveBackslashUnlessRoot(ExtractFileDir(InstallRoot)); - if DataRoot = '' then - DataRoot := InstallRoot; - - ScriptFile := ExpandConstant('{tmp}\nvm-uninstall-force-close.ps1'); - SaveStringToFile( - ScriptFile, - BuildStopProcessesUnderRootsPowerShell(AppRoot, DataRoot), - False - ); - - if Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + ScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Log('Force-close script completed with exit code ' + IntToStr(ResultCode) + '.') - else - Log('Force-close script failed to start.'); - - DeleteFile(ScriptFile); -end; - -procedure ScanKnownLockedShims(const AppRoot: String); -var - TrimmedAppRoot: String; -begin - TrimmedAppRoot := Trim(AppRoot); - if TrimmedAppRoot = '' then - Exit; - - NotePathLeftoverIfExists('nvm executable', TrimmedAppRoot + '\' + ExpandConstant('{#Alias}') + '.exe'); - NotePathLeftoverIfExists('proxy shim', TrimmedAppRoot + '\utils\proxy.exe'); - NotePathLeftoverIfExists('sync service', TrimmedAppRoot + '\utils\sync.exe'); - NotePathLeftoverIfExists('node shim', TrimmedAppRoot + '\.shim\node.exe'); - NotePathLeftoverIfExists('npm shim', TrimmedAppRoot + '\.shim\npm.exe'); - NotePathLeftoverIfExists('npx shim', TrimmedAppRoot + '\.shim\npx.exe'); -end; - -procedure ScanRegistryLeftovers(); -var - PrefsKey: String; - NVMHome: String; -begin - PrefsKey := 'Software\{#OrgLabel}\Preferences\{#Alias}'; - - if RegKeyExists(HKCU, PrefsKey) then - AppendUninstallLeftover('Registry (HKCU): ' + PrefsKey); - - if RegValueExists(HKLM, PrefsKey, 'AccessToken') or - RegValueExists(HKLM, PrefsKey, 'AccessKey') or - RegValueExists(HKLM, PrefsKey, 'JwksCose') then - AppendUninstallLeftover('Registry (HKLM licensing; run uninstall as admin to remove): ' + PrefsKey); - - if RegValueExists(HKCU, 'Environment', 'NVM_HOME') then - begin - if RegQueryStringValue(HKCU, 'Environment', 'NVM_HOME', NVMHome) then - AppendUninstallLeftover('Environment variable NVM_HOME: ' + NVMHome); - end; -end; - -procedure AppendRunningNvmProcessesToLeftovers( - const AppRoot, InstallRoot: String -); -var - ProcessList: String; -begin - if not ScanBlockingUninstallProcesses(AppRoot, InstallRoot, ProcessList) then - Exit; - - ProcessList := Trim(ProcessList); - if ProcessList = '' then - Exit; - - AppendUninstallLeftover('Running NVM processes blocked removal:' + #13#10 + ProcessList); -end; - -function IsIgnorableUninstallLeftoverLine(const Line: String): Boolean; -var - Trimmed: String; -begin - Trimmed := Trim(Line); - Result := - (Trimmed = '') or - (CompareText(Trimmed, 'Running NVM processes:') = 0) or - (CompareText(Trimmed, 'Running NVM processes blocked removal:') = 0); -end; - -function BuildUninstallLeftoverDisplayReport(const Report: String): String; -var - LineStart, LineEnd, LineLength: Integer; - Line: String; -begin - Result := ''; - LineStart := 1; - while LineStart <= Length(Report) do - begin - LineEnd := LineStart; - while (LineEnd <= Length(Report)) and (Report[LineEnd] <> #10) and (Report[LineEnd] <> #13) do - LineEnd := LineEnd + 1; - - LineLength := LineEnd - LineStart; - if LineLength > 0 then - begin - Line := StripUtf8Bom(Trim(Copy(Report, LineStart, LineLength))); - if not IsIgnorableUninstallLeftoverLine(Line) then - begin - if Result <> '' then - Result := Result + #13#10; - Result := Result + Line; - end; - end; - - LineStart := LineEnd + 1; - if (LineStart <= Length(Report)) and (Report[LineStart] = #10) then - LineStart := LineStart + 1; - end; -end; - -procedure ScanUninstallLeftovers(); -var - AppRoot: String; - InstallRoot: String; -begin - UninstallLeftovers := ''; - - AppRoot := Trim(UninstallAppRoot); - if AppRoot = '' then - AppRoot := ExpandConstant('{app}'); - AppRoot := RemoveBackslashUnlessRoot(AppRoot); - - InstallRoot := Trim(UninstallInstallRoot); - if InstallRoot = '' then - InstallRoot := GetInstallRootForUninstall(''); - InstallRoot := RemoveBackslashUnlessRoot(InstallRoot); - - ScanRegistryLeftovers(); - - if SyncTaskStillExists() then - AppendUninstallLeftover('Scheduled task: ' + SyncTaskName); - - { Delayed app-folder cleanup is expected; do not warn about paths/processes there. } - if UninstallDelayedCleanupScheduled then - Exit; - - NotePathLeftoverIfExists('Program folder', AppRoot); - - if (InstallRoot <> '') and (not PathIsUnderOrEqualRoot(InstallRoot, AppRoot)) then - NotePathLeftoverIfExists('Node.js versions folder', InstallRoot); - - ScanKnownLockedShims(AppRoot); - // Scan DataRoot so .shim under split storage is included (not InstallRoot alone). - if InstallRoot <> '' then - begin - if RemoveBackslashUnlessRoot(ExtractFileDir(InstallRoot)) <> '' then - AppendRunningNvmProcessesToLeftovers( - AppRoot, - RemoveBackslashUnlessRoot(ExtractFileDir(InstallRoot)) - ) - else - AppendRunningNvmProcessesToLeftovers(AppRoot, InstallRoot); - end - else - AppendRunningNvmProcessesToLeftovers(AppRoot, InstallRoot); -end; - -function InitializeUninstall(): Boolean; -begin - ResetUninstallLeftovers(); - UninstallAppRoot := ExpandConstant('{app}'); - UninstallInstallRoot := GetInstallRootForUninstall(''); - UninstallSelfExe := ExpandConstant('{uninstallexe}'); - - if UninstallSilent() then - begin - Result := True; - Exit; - end; - - Result := ConfirmUninstallRemoveNodeVersions(UninstallInstallRoot); - if not Result then - Exit; - - Result := ConfirmUninstallBlockingProcesses(); -end; - -procedure DeinitializeUninstall(); -var - DisplayReport: String; -begin - ScanUninstallLeftovers(); - DisplayReport := Trim(BuildUninstallLeftoverDisplayReport(Trim(UninstallLeftovers))); - - if DisplayReport = '' then - Exit; - - Log('Uninstall leftovers:' + #13#10 + DisplayReport); - - if UninstallSilent() then - Exit; - - MsgBox( - 'Some NVM for Windows data could not be removed:' + #13#10 + #13#10 + - DisplayReport + #13#10 + #13#10 + - 'Remove these manually to complete uninstall.' + #13#10 + - 'Log: ' + ExpandConstant('{log}'), - mbInformation, - MB_OK - ); -end; - -procedure RemoveAllNodeVersionsWindowsAppsEntries(); -var - SubKeys: TArrayOfString; - I: Integer; - KeyName: String; -begin - if not RegGetSubkeyNames(HKCU, WindowsAppsUninstallRoot, SubKeys) then - Exit; - - for I := 0 to GetArrayLength(SubKeys) - 1 do - begin - KeyName := SubKeys[I]; - if Pos('nvm4w-node-v', KeyName) = 1 then - RegDeleteKeyIncludingSubkeys(HKCU, WindowsAppsUninstallRoot + '\' + KeyName); - end; -end; - -function GetInstallRootForUninstall(Param: String): String; -begin - Result := ''; - if not RegQueryStringValue(HKCU, '{#RegistryKey}', 'InstallRoot', Result) then - Result := GetRealInstallRoot(''); - Result := Trim(Result); -end; - -procedure CleanupInstallRootOnUninstall(); -var - RootToRemove: String; -begin - RootToRemove := Trim(UninstallInstallRoot); - if RootToRemove = '' then - RootToRemove := GetInstallRootForUninstall(''); - - if RootToRemove = '' then - Exit; - - if not DirExists(RootToRemove) then - Exit; - - if not IsSafeRemovableDirectory(RootToRemove) then - begin - Log('Skipping install root removal due to safety check: ' + RootToRemove); - Exit; - end; - - ForceCloseNvmProcessesOnUninstall(); - - if DelTree(RootToRemove, True, True, True) then - Log('Removed install root directory: ' + RootToRemove) - else - begin - Log('Install root removal failed after force-close, retrying: ' + RootToRemove); - ForceCloseNvmProcessesOnUninstall(); - - if DelTree(RootToRemove, True, True, True) then - Log('Removed install root directory on retry: ' + RootToRemove) - else - Log('Failed to remove install root directory: ' + RootToRemove); - end; -end; - -procedure CleanupAppRootOnUninstall(); -var - AppRoot: String; - OrgRoot: String; - CleanupScriptFile: String; - CleanupScript: String; - AppRootEscaped: String; - OrgRootEscaped: String; - ResultCode: Integer; -begin - AppRoot := Trim(UninstallAppRoot); - if AppRoot = '' then - AppRoot := ExpandConstant('{app}'); - - AppRoot := RemoveBackslashUnlessRoot(AppRoot); - - if (AppRoot = '') or (not DirExists(AppRoot)) then - Exit; - - OrgRoot := RemoveBackslashUnlessRoot(ExtractFileDir(AppRoot)); - - if not IsSafeRemovableDirectory(AppRoot) then - begin - Log('Skipping app root removal due to safety check: ' + AppRoot); - Exit; - end; - - ForceCloseNvmProcessesOnUninstall(); - - if DelTree(AppRoot, True, True, True) then - begin - Log('Removed app root directory: ' + AppRoot); - - if (OrgRoot <> '') and DirExists(OrgRoot) and IsSafeRemovableDirectory(OrgRoot) then - begin - if RemoveDir(OrgRoot) then - Log('Removed empty org root directory: ' + OrgRoot) - else - Log('Org root not removed (not empty or in use): ' + OrgRoot); - end; - - Exit; - end; - - Log('App root removal failed after force-close, retrying: ' + AppRoot); - ForceCloseNvmProcessesOnUninstall(); - - if DelTree(AppRoot, True, True, True) then - begin - Log('Removed app root directory on retry: ' + AppRoot); - - if (OrgRoot <> '') and DirExists(OrgRoot) and IsSafeRemovableDirectory(OrgRoot) then - begin - if RemoveDir(OrgRoot) then - Log('Removed empty org root directory: ' + OrgRoot) - else - Log('Org root not removed (not empty or in use): ' + OrgRoot); - end; - - Exit; - end; - - if (OrgRoot <> '') and (not IsSafeRemovableDirectory(OrgRoot)) then - begin - Log('Skipping org root removal due to safety check: ' + OrgRoot); - OrgRoot := ''; - end; - - Log('Immediate app root removal failed, scheduling delayed cleanup: ' + AppRoot); - UninstallDelayedCleanupScheduled := True; - - AppRootEscaped := EscapeSingleQuotedPowerShellString(AppRoot); - OrgRootEscaped := EscapeSingleQuotedPowerShellString(OrgRoot); - CleanupScriptFile := ExpandConstant('{tmp}\nvm-uninstall-app-cleanup.ps1'); - CleanupScript := - BuildStopProcessesUnderRootsPowerShell( - AppRoot, - RemoveBackslashUnlessRoot(ExtractFileDir(GetInstallRootForUninstall(''))) - ) + #13#10 + - '$app = ''' + AppRootEscaped + '''' + #13#10 + - '$org = ''' + OrgRootEscaped + '''' + #13#10 + - 'Start-Sleep -Seconds 2' + #13#10 + - 'if (Test-Path -LiteralPath $app) { Remove-Item -LiteralPath $app -Recurse -Force -ErrorAction SilentlyContinue }' + #13#10 + - 'if ($org -ne '''') {' + #13#10 + - ' if (Test-Path -LiteralPath $org) {' + #13#10 + - ' $items = @(Get-ChildItem -LiteralPath $org -Force -ErrorAction SilentlyContinue)' + #13#10 + - ' if ($items.Count -eq 0) { Remove-Item -LiteralPath $org -Force -ErrorAction SilentlyContinue }' + #13#10 + - ' }' + #13#10 + - '}'; - - SaveStringToFile(CleanupScriptFile, CleanupScript, False); - Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + CleanupScriptFile + '"', - '', - SW_HIDE, - ewNoWait, - ResultCode - ); -end; - -procedure CleanupSettingsRegistryOnUninstall(); -var - RegistryCleanupScriptFile: String; - RegistryCleanupScript: String; - ResultCode: Integer; -begin - RegistryCleanupScriptFile := ExpandConstant('{tmp}\nvm-registry-cleanup.ps1'); - RegistryCleanupScript := - '$orgKey = ''Software\{#OrgLabel}''' + #13#10 + - '$preferencesKey = $orgKey + ''\Preferences''' + #13#10 + - '$aliasKey = $preferencesKey + ''\{#Alias}''' + #13#10 + - '$hkcu = [Microsoft.Win32.Registry]::CurrentUser' + #13#10 + - '$hklm = [Microsoft.Win32.Registry]::LocalMachine' + #13#10 + - 'foreach ($valueName in @(''AccessToken'',''AccessKey'',''JwksCose'')) {' + #13#10 + - ' try {' + #13#10 + - ' $k = $hklm.OpenSubKey($aliasKey, $true)' + #13#10 + - ' if ($null -ne $k) { $k.DeleteValue($valueName, $false); $k.Close() }' + #13#10 + - ' } catch {}' + #13#10 + - '}' + #13#10 + - 'try { $hkcu.DeleteSubKeyTree($aliasKey, $false) } catch {}' + #13#10 + - 'function Remove-IfEmpty([Microsoft.Win32.RegistryKey]$root, [string]$path) {' + #13#10 + - ' $k = $root.OpenSubKey($path, $true)' + #13#10 + - ' if ($null -eq $k) { return }' + #13#10 + - ' $isEmpty = ($k.SubKeyCount -eq 0) -and ($k.ValueCount -eq 0)' + #13#10 + - ' $k.Close()' + #13#10 + - ' if ($isEmpty) {' + #13#10 + - ' try { $root.DeleteSubKey($path, $false) } catch {}' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - 'Remove-IfEmpty -root $hkcu -path $preferencesKey' + #13#10 + - 'Remove-IfEmpty -root $hkcu -path $orgKey' + #13#10 + - 'Remove-IfEmpty -root $hklm -path $aliasKey' + #13#10 + - 'Remove-IfEmpty -root $hklm -path $preferencesKey' + #13#10 + - 'Remove-IfEmpty -root $hklm -path $orgKey'; - - SaveStringToFile(RegistryCleanupScriptFile, RegistryCleanupScript, False); - Exec( - ExpandConstant('{cmd}'), - '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + RegistryCleanupScriptFile + '"', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ); - DeleteFile(RegistryCleanupScriptFile); - - if ResultCode <> 0 then - Log('Registry cleanup script returned exit code ' + IntToStr(ResultCode) + '.'); -end; - -procedure ClearMachineLicensingOnUninstall(); -var - NVMExe: String; - ResultCode: Integer; -begin - NVMExe := ExpandConstant('{app}\{#Alias}.exe'); - if not FileExists(NVMExe) then - begin - Log('Skipping machine licensing cleanup; nvm.exe not found at ' + NVMExe); - Exit; - end; - - if Exec(NVMExe, '--clear-machine-licensing', ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - if ResultCode <> 0 then - Log('Machine licensing cleanup returned exit code ' + IntToStr(ResultCode) + '.'); - end - else - Log('Failed to run machine licensing cleanup.'); -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -begin - if CurUninstallStep = usUninstall then - begin - UninstallAppRoot := ExpandConstant('{app}'); - UninstallInstallRoot := GetInstallRootForUninstall(''); - ForceCloseNvmProcessesOnUninstall(); - CleanupInstallRootOnUninstall(); - RemoveSyncTaskOnUninstall(); - RemoveAllNodeVersionsWindowsAppsEntries(); - ClearMachineLicensingOnUninstall(); - end; - - if CurUninstallStep = usPostUninstall then - begin - CleanupSettingsRegistryOnUninstall(); - CleanupInstallRootOnUninstall(); - end; - - if CurUninstallStep = usDone then - CleanupAppRootOnUninstall(); -end; \ No newline at end of file diff --git a/installer/tests/OfficialNodeTest.iss b/installer/tests/OfficialNodeTest.iss deleted file mode 100644 index 44a77ef7..00000000 --- a/installer/tests/OfficialNodeTest.iss +++ /dev/null @@ -1,149 +0,0 @@ -; Compiles the real officialnode.iss choices without uninstalling Node.js. -; The PowerShell suite passes /OFFICIALNODELOG so Drop records msiexec instead of launching it. - -#define Alias "nvm" - -[Setup] -AppId={{7E4C1A2B-9D10-4F55-A111-0FF1C1A10002} -AppName=NVM Official Node Choice Test -AppVersion=1.0.0 -DefaultDirName={localappdata}\nvm-officialnode-inno-test -PrivilegesRequired=lowest -OutputBaseFilename=officialnode-choice-test -DisableDirPage=yes -DisableProgramGroupPage=yes -Uninstallable=no -CreateAppDir=yes -Compression=none -SolidCompression=no -CloseApplications=no - -[Code] -var - OfficialNodePage: TInputOptionWizardPage; - OfficialNodeDetected: Boolean; - OfficialNodePath: String; - OfficialNodeVersion: String; - OfficialNodeProductCode: String; - OfficialNodeUninstallString: String; - OfficialNodeIsMsi: Boolean; - OfficialNodeModuleCount: Integer; - OfficialNodeModuleSize: Int64; - OfficialNodeAction: String; - OfficialNodeCopyFailed: Boolean; - OfficialNodeDropFailed: Boolean; - OfficialNodeDropError: String; - InstallerHardFailed: Boolean; - SuppressStepFailure: Boolean; - StepBudgetArmed: Boolean; - BudgetDeadline: Cardinal; - BudgetCode: String; - BudgetMessage: String; - DeferOfficialNodeOnV2Upgrade: Boolean; - -#include "..\installtimeout.iss" - -function NormalizePath(const PathValue: String): String; -begin - Result := Trim(PathValue); - while (Length(Result) > 3) and (Result[Length(Result)] = '\') do - Result := Copy(Result, 1, Length(Result) - 1); -end; - -function GetRealProgramRoot(Param: String): String; -begin - Result := ExpandConstant('{localappdata}\Author Software\nvm'); -end; - -function GetInstallRoot(Param: String): String; -begin - Result := ExpandConstant('{param:INSTALLROOT}'); -end; - -procedure AppendInstallLog(const Message: String); -var - LogPath: String; -begin - LogPath := Trim(ExpandConstant('{param:INSTALLLOG}')); - if LogPath <> '' then - SaveStringToFile(LogPath, Message + #13#10, True); -end; - -procedure AppendInstallLogWarn(const Message: String); -begin - AppendInstallLog('WARN ' + Message); -end; - -function CopyTreeWithProgress( - const SourceDir, DestDir: String; - ProgressPage: TOutputProgressWizardPage; - var CopiedCount: Integer; - const TotalCount: Integer; - const VersionLabel: String -): Boolean; -var - ResultCode: Integer; -begin - Result := False; - if not ForceDirectories(DestDir) then - Exit; - if not Exec( - 'robocopy.exe', - '"' + SourceDir + '" "' + DestDir + '" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ) then - Exit; - Result := ResultCode < 8; - if Result then - CopiedCount := CopiedCount + 1; -end; - -#include "..\officialnode.iss" - -procedure InitializeWizard(); -begin - OfficialNodeDetected := ExpandConstant('{param:NODEFOUND}') = '1'; - OfficialNodePath := ExpandConstant('{param:NODESOURCE}'); - OfficialNodeVersion := '22.14.0'; - OfficialNodeUninstallString := ''; - OfficialNodeIsMsi := True; - OfficialNodeModuleCount := 1; - OfficialNodeModuleSize := 1024; - OfficialNodeCopyFailed := False; - OfficialNodeDropFailed := False; - OfficialNodeDropError := ''; - OfficialNodeAction := OfficialNodeParam(); - if ExpandConstant('{param:SKIPPRODUCT}') = '1' then - OfficialNodeProductCode := '' - else - OfficialNodeProductCode := '{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}'; - if OfficialNodeDetected then - CreateOfficialNodePage(); -end; - -procedure CurStepChanged(CurStep: TSetupStep); -begin - if CurStep <> ssPostInstall then - Exit; - ApplyOfficialNodeChoice(); -end; - -procedure DeinitializeSetup(); -var - Path: String; - Body: String; -begin - Path := Trim(ExpandConstant('{param:OFFICIALNODERESULT}')); - if Path = '' then - Exit; - Body := - 'action=' + OfficialNodeAction + #13#10 + - 'detected=' + IntToStr(Integer(OfficialNodeDetected)) + #13#10 + - 'copyfailed=' + IntToStr(Integer(OfficialNodeCopyFailed)) + #13#10 + - 'dropfailed=' + IntToStr(Integer(OfficialNodeDropFailed)) + #13#10 + - 'error=' + OfficialNodeDropError + #13#10; - SaveStringToFile(Path, Body, False); -end; diff --git a/installer/tests/Test-OfficialNodeChoices.ps1 b/installer/tests/Test-OfficialNodeChoices.ps1 deleted file mode 100644 index 76210124..00000000 --- a/installer/tests/Test-OfficialNodeChoices.ps1 +++ /dev/null @@ -1,165 +0,0 @@ -# Compiles the Inno Setup official-Node choices and runs adopt and drop -# against temporary files. Drop records msiexec instead of uninstalling Node.js. -# Usage: powershell -NoProfile -File installer\tests\Test-OfficialNodeChoices.ps1 - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$testsRoot = Split-Path -Parent $MyInvocation.MyCommand.Path -$iss = Join-Path $testsRoot "OfficialNodeTest.iss" -$iscc = Join-Path $env:LOCALAPPDATA "Programs\Inno Setup 6\ISCC.exe" -if (-not (Test-Path -LiteralPath $iscc)) { - throw "Inno Setup compiler not found: $iscc" -} - -$failures = New-Object System.Collections.Generic.List[string] -$productCode = "{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}" - -function Add-Failure([string]$Message) { - $failures.Add($Message) - Write-Host "FAIL $Message" -} - -function Assert-True([bool]$Condition, [string]$Message) { - if ($Condition) { - Write-Host "ok $Message" - return - } - Add-Failure $Message -} - -$compileDir = Join-Path ([System.IO.Path]::GetTempPath()) ("nvm-inno-officialnode-" + [guid]::NewGuid().ToString("N")) -New-Item -ItemType Directory -Force -Path $compileDir | Out-Null -& $iscc "/O$compileDir" "/Q" $iss -if ($LASTEXITCODE -ne 0) { - throw "ISCC failed with exit $LASTEXITCODE" -} -$setup = Join-Path $compileDir "officialnode-choice-test.exe" -if (-not (Test-Path -LiteralPath $setup)) { - throw "Test installer was not created: $setup" -} - -function Read-Result([string]$Path) { - $map = @{} - if (-not (Test-Path -LiteralPath $Path)) { - return $map - } - foreach ($line in Get-Content -LiteralPath $Path) { - $parts = $line -split "=", 2 - if ($parts.Count -eq 2) { - $map[$parts[0]] = $parts[1] - } - } - return $map -} - -function Invoke-InnoChoice { - param( - [string]$OfficialNode, - [switch]$Found, - [switch]$MissingNode, - [switch]$SkipProduct - ) - - $case = Join-Path $compileDir ("case-" + [guid]::NewGuid().ToString("N")) - $source = Join-Path $case "official" - $modules = Join-Path $case "npm-modules\left-pad" - $installs = Join-Path $case "installs" - $app = Join-Path $case "app" - New-Item -ItemType Directory -Force -Path $source, $modules, $installs, $app | Out-Null - if (-not $MissingNode) { - Set-Content -LiteralPath (Join-Path $source "node.exe") -Value "node" -Encoding ascii - } - Set-Content -LiteralPath (Join-Path $source "node.lib") -Value "lib" -Encoding ascii - Set-Content -LiteralPath (Join-Path $modules "index.js") -Value "module" -Encoding ascii - - $resultPath = Join-Path $case "result.txt" - $logPath = Join-Path $case "uninstall.txt" - $installLog = Join-Path $case "install.log" - $args = @( - "/VERYSILENT", - "/SUPPRESSMSGBOXES", - "/NORESTART", - "/DIR=$app", - "/OFFICIALNODE=$OfficialNode", - "/NODESOURCE=$source", - "/INSTALLROOT=$installs", - "/OFFICIALNODEMODULES=$(Split-Path -Parent $modules)", - "/OFFICIALNODELOG=$logPath", - "/OFFICIALNODERESULT=$resultPath", - "/INSTALLLOG=$installLog" - ) - if ($Found) { $args += "/NODEFOUND=1" } - if ($SkipProduct) { $args += "/SKIPPRODUCT=1" } - - $proc = Start-Process -FilePath $setup -ArgumentList $args -Wait -PassThru - $destNode = Join-Path $installs "v22.14.0\node.exe" - $destLib = Join-Path $installs "v22.14.0\node.lib" - $destModule = Join-Path $installs "v22.14.0\node_modules\left-pad\index.js" - $copied = (Test-Path -LiteralPath $destNode) -and (Test-Path -LiteralPath $destLib) -and (Test-Path -LiteralPath $destModule) - $uninstallText = "" - if (Test-Path -LiteralPath $logPath) { - $uninstallText = (Get-Content -LiteralPath $logPath -Raw).Trim() - } - [pscustomobject]@{ - Exit = $proc.ExitCode - Result = Read-Result $resultPath - Copied = $copied - SourceRemains = Test-Path -LiteralPath (Join-Path $source "node.lib") - Uninstall = $uninstallText - InstallLog = if (Test-Path -LiteralPath $installLog) { Get-Content -LiteralPath $installLog -Raw } else { "" } - } -} - -try { - $adopt = Invoke-InnoChoice "adopt" -Found - Assert-True ($adopt.Exit -eq 0) "adopt exits 0" - Assert-True ($adopt.Result["action"] -eq "adopt") "adopt keeps the adopt action" - Assert-True ($adopt.Result["copyfailed"] -eq "0" -and $adopt.Result["dropfailed"] -eq "0") "adopt copy and uninstall succeed" - Assert-True $adopt.Copied "adopt copies Node.js and global modules into v22.14.0" - Assert-True $adopt.SourceRemains "adopt leaves the official tree in place for uninstall" - Assert-True ($adopt.Uninstall -match [regex]::Escape($productCode) -and $adopt.Uninstall -match "/x" -and $adopt.Uninstall -match "/qn") "adopt records a silent msiexec uninstall" - - $drop = Invoke-InnoChoice "drop" -Found - Assert-True ($drop.Exit -eq 0) "drop exits 0" - Assert-True ($drop.Result["action"] -eq "drop") "drop keeps the drop action" - Assert-True ($drop.Result["copyfailed"] -eq "0" -and $drop.Result["dropfailed"] -eq "0") "drop uninstall succeeds without a copy failure" - Assert-True (-not $drop.Copied) "drop does not copy official Node.js" - Assert-True $drop.SourceRemains "drop leaves the official tree for the recorded uninstall" - Assert-True ($drop.Uninstall -match [regex]::Escape($productCode) -and $drop.Uninstall -match "/x" -and $drop.Uninstall -match "/qn") "drop records a silent msiexec uninstall" - - $ignore = Invoke-InnoChoice "ignore" -Found - Assert-True ($ignore.Exit -eq 0 -and $ignore.Result["action"] -eq "adopt" -and $ignore.Copied) "ignore is treated as adopt" - - $other = Invoke-InnoChoice "adopt-keep" -Found - Assert-True ($other.Exit -eq 0 -and $other.Result["action"] -eq "adopt" -and $other.Copied) "unsupported values are treated as adopt" - - $absent = Invoke-InnoChoice "drop" - Assert-True ($absent.Exit -eq 0) "missing official Node exits 0" - Assert-True (-not $absent.Copied -and [string]::IsNullOrWhiteSpace($absent.Uninstall)) "missing official Node does not copy or uninstall" - - $broken = Invoke-InnoChoice "adopt" -Found -MissingNode - Assert-True ($broken.Exit -eq 0) "adopt with a missing node.exe still removes official Node" - Assert-True ($broken.Result["copyfailed"] -eq "1") "adopt records the failed copy" - Assert-True (-not $broken.Copied) "a failed copy does not leave a Node.js version" - Assert-True ($broken.Uninstall -match "/x") "a failed copy still uninstalls official Node" - - $noCommand = Invoke-InnoChoice "drop" -Found -SkipProduct - Assert-True ($noCommand.Exit -ne 0) "drop without an uninstall command fails the install" - Assert-True ($noCommand.InstallLog -match "No uninstall command") "drop without an uninstall command records the failure" - Assert-True (-not $noCommand.Copied -and [string]::IsNullOrWhiteSpace($noCommand.Uninstall)) "a failed drop does not copy or launch uninstall" -} finally { - if (Test-Path -LiteralPath $compileDir) { - Remove-Item -LiteralPath $compileDir -Recurse -Force -ErrorAction SilentlyContinue - } -} - -if ($failures.Count -gt 0) { - Write-Host "" - Write-Host "$($failures.Count) failure(s)" - exit 1 -} - -Write-Host "" -Write-Host "Inno official Node choices passed" -exit 0 diff --git a/installer/updater/build.ps1 b/installer/updater/build.ps1 deleted file mode 100644 index d545e712..00000000 --- a/installer/updater/build.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -param( - [string]$OutputExe = "bin\nvm-upgrader.exe", - [ValidateSet("ReleaseSmall", "ReleaseSafe", "ReleaseFast", "Debug")] - [string]$BuildProfile = "ReleaseSmall", - [ValidateSet("amd64", "arm64")] - [string]$Architecture -) - -$ErrorActionPreference = "Stop" - -if ([string]::IsNullOrWhiteSpace($Architecture)) { - if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - $Architecture = "arm64" - } else { - $Architecture = "amd64" - } -} - -$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path -$outputPath = [System.IO.Path]::GetFullPath((Join-Path $scriptRoot $OutputExe)) -$outputDir = Split-Path -Parent $outputPath - -if (!(Test-Path $outputDir)) { - New-Item -ItemType Directory -Path $outputDir -Force | Out-Null -} - -Push-Location $scriptRoot -try { - # x86_64-windows-gnu if compiling on Linux, otherwise x86_64-windows-msvc - zig build-exe -target $(if ($Architecture -eq 'arm64') { 'aarch64-windows-msvc' } else { 'x86_64-windows-msvc' }) .\main.zig -O $BuildProfile -fstrip -femit-bin=nvm-upgrader - Copy-Item -Force .\nvm-upgrader $outputPath - go-winres patch --in .\winres\winres.json --no-backup $outputPath -} -finally { - Remove-Item .\nvm-upgrader -ErrorAction SilentlyContinue - Remove-Item .\nvm-upgrader.pdb -ErrorAction SilentlyContinue - Pop-Location -} diff --git a/installer/updater/main.zig b/installer/updater/main.zig deleted file mode 100644 index 76c3f459..00000000 --- a/installer/updater/main.zig +++ /dev/null @@ -1,642 +0,0 @@ -//! nvm-upgrader.exe β€” Elevated one-time cleanup helper for NVM for Windows v1 β†’ v2 migration. -//! -//! Usage (all flags are optional; missing flags skip the corresponding step): -//! nvm-upgrader.exe [--install-dir ] [--symlink-path ] [--new-nvm ] -//! -//! The embedded manifest declares requireAdministrator so Windows auto-elevates via UAC. -//! The installer calls this once for all privileged cleanup, then EnsureNvmPathPriority -//! writes the new v2 NVM_HOME back into HKCU immediately afterwards. - -const std = @import("std"); -const windows = std.os.windows; - -// ── Win32 types ─────────────────────────────────────────────────────────────── - -const HKEY = windows.HKEY; -const DWORD = u32; -const LSTATUS = i32; -const REGSAM = DWORD; -const BOOL = windows.BOOL; -/// Read-only null-terminated wide string. -const LPCWSTR = [*:0]const u16; -/// Writable wide-char buffer (output parameters). -const LPWSTR = [*]u16; - -const KEY_READ: REGSAM = 0x20019; -const KEY_WRITE: REGSAM = 0x20006; -const REG_SZ: DWORD = 1; -const REG_EXPAND_SZ: DWORD = 2; -const FILE_ATTRIBUTE_REPARSE_POINT: DWORD = 0x0400; -const INVALID_FILE_ATTRIBUTES: DWORD = 0xFFFFFFFF; -const ERROR_SUCCESS: LSTATUS = 0; -const ERROR_NO_MORE_ITEMS: LSTATUS = 259; -const CREATE_NO_WINDOW: DWORD = 0x08000000; -const INFINITE: DWORD = 0xFFFFFFFF; - -// ── Win32 imports ───────────────────────────────────────────────────────────── - -extern "advapi32" fn RegOpenKeyExW( - hKey: HKEY, - lpSubKey: LPCWSTR, - ulOptions: DWORD, - samDesired: REGSAM, - phkResult: *HKEY, -) callconv(.winapi) LSTATUS; - -extern "advapi32" fn RegCloseKey(hKey: HKEY) callconv(.winapi) LSTATUS; - -extern "advapi32" fn RegDeleteValueW( - hKey: HKEY, - lpValueName: LPCWSTR, -) callconv(.winapi) LSTATUS; - -extern "advapi32" fn RegQueryValueExW( - hKey: HKEY, - lpValueName: LPCWSTR, - lpReserved: ?*DWORD, - lpType: ?*DWORD, - lpData: ?[*]u8, - lpcbData: *DWORD, -) callconv(.winapi) LSTATUS; - -extern "advapi32" fn RegSetValueExW( - hKey: HKEY, - lpValueName: LPCWSTR, - Reserved: DWORD, - dwType: DWORD, - lpData: [*]const u8, - cbData: DWORD, -) callconv(.winapi) LSTATUS; - -extern "advapi32" fn RegEnumKeyExW( - hKey: HKEY, - dwIndex: DWORD, - lpName: LPWSTR, - lpcchName: *DWORD, - lpReserved: ?*DWORD, - lpClass: ?LPWSTR, - lpcchClass: ?*DWORD, - lpftLastWriteTime: ?*anyopaque, -) callconv(.winapi) LSTATUS; - -/// Recursively delete a registry key and all its subkeys. -extern "shlwapi" fn SHDeleteKeyW(hkey: HKEY, pszSubKey: LPCWSTR) callconv(.winapi) LSTATUS; - -extern "kernel32" fn GetFileAttributesW(lpFileName: LPCWSTR) callconv(.winapi) DWORD; -extern "kernel32" fn RemoveDirectoryW(lpPathName: LPCWSTR) callconv(.winapi) BOOL; - -const STARTUPINFOW = extern struct { - cb: DWORD, - lpReserved: ?LPWSTR, - lpDesktop: ?LPWSTR, - lpTitle: ?LPWSTR, - dwX: DWORD, - dwY: DWORD, - dwXSize: DWORD, - dwYSize: DWORD, - dwXCountChars: DWORD, - dwYCountChars: DWORD, - dwFillAttribute: DWORD, - dwFlags: DWORD, - wShowWindow: u16, - cbReserved2: u16, - lpReserved2: ?[*]u8, - hStdInput: ?windows.HANDLE, - hStdOutput: ?windows.HANDLE, - hStdError: ?windows.HANDLE, -}; - -const PROCESS_INFORMATION = extern struct { - hProcess: windows.HANDLE, - hThread: windows.HANDLE, - dwProcessId: DWORD, - dwThreadId: DWORD, -}; - -extern "kernel32" fn CreateProcessW( - lpApplicationName: ?LPCWSTR, - lpCommandLine: LPWSTR, - lpProcessAttributes: ?*anyopaque, - lpThreadAttributes: ?*anyopaque, - bInheritHandles: BOOL, - dwCreationFlags: DWORD, - lpEnvironment: ?*anyopaque, - lpCurrentDirectory: ?LPCWSTR, - lpStartupInfo: *STARTUPINFOW, - lpProcessInformation: *PROCESS_INFORMATION, -) callconv(.winapi) BOOL; - -extern "kernel32" fn WaitForSingleObject( - hHandle: windows.HANDLE, - dwMilliseconds: DWORD, -) callconv(.winapi) DWORD; - -extern "kernel32" fn CloseHandle(hObject: windows.HANDLE) callconv(.winapi) BOOL; - -// ── Upgrade log ─────────────────────────────────────────────────────────────── -// A best-effort in-memory log that is flushed to \upgrade.log -// only when at least one error is recorded. - -var g_log_buf: std.ArrayListUnmanaged(u8) = .{}; -var g_log_alloc: std.mem.Allocator = undefined; -var g_has_errors: bool = false; - -fn logMsg(comptime fmt: []const u8, args: anytype) void { - const line = std.fmt.allocPrint(g_log_alloc, fmt ++ "\r\n", args) catch return; - defer g_log_alloc.free(line); - g_log_buf.appendSlice(g_log_alloc, line) catch {}; -} - -fn logError(comptime fmt: []const u8, args: anytype) void { - g_has_errors = true; - logMsg("[ERROR] " ++ fmt, args); -} - -fn flushUpgradeLog(install_dir: []const u8) void { - if (!g_has_errors) return; - if (g_log_buf.items.len == 0) return; - const log_path = std.fmt.allocPrint(g_log_alloc, "{s}\\upgrade.log", .{install_dir}) catch return; - defer g_log_alloc.free(log_path); - const file = std.fs.createFileAbsolute(log_path, .{ .truncate = true }) catch return; - defer file.close(); - file.writeAll(g_log_buf.items) catch {}; -} - -// ── String helpers ──────────────────────────────────────────────────────────── - -/// Heap-allocate a null-terminated UTF-16LE copy of a UTF-8 string. -fn toW(alloc: std.mem.Allocator, s: []const u8) ![:0]u16 { - return std.unicode.utf8ToUtf16LeAllocZ(alloc, s); -} - -/// Return a heap-allocated, lowercased copy of `path` with: -/// - forward slashes replaced by backslashes -/// - trailing backslash stripped (unless a bare root like "c:\") -fn normPath(alloc: std.mem.Allocator, path: []const u8) ![]u8 { - const trimmed = std.mem.trim(u8, path, " \t\r\n"); - // Determine final length (strip trailing backslash for non-root paths). - var end = trimmed.len; - while (end > 3 and trimmed[end - 1] == '\\') end -= 1; - while (end > 3 and trimmed[end - 1] == '/') end -= 1; - const result = try alloc.alloc(u8, end); - @memcpy(result, trimmed[0..end]); - std.mem.replaceScalar(u8, result, '/', '\\'); - for (result) |*c| c.* = std.ascii.toLower(c.*); - return result; -} - -// ── Registry helpers ────────────────────────────────────────────────────────── - -/// Open a registry key. Returns null on failure; caller must RegCloseKey the result. -fn openKey(alloc: std.mem.Allocator, root: HKEY, sub: []const u8, write: bool) ?HKEY { - const sub_w = toW(alloc, sub) catch return null; - defer alloc.free(sub_w); - const access = if (write) KEY_READ | KEY_WRITE else KEY_READ; - var hk: HKEY = undefined; - if (RegOpenKeyExW(root, sub_w.ptr, 0, access, &hk) != ERROR_SUCCESS) return null; - return hk; -} - -/// Delete a named value from a registry key path. Errors are ignored. -fn deleteRegValue(alloc: std.mem.Allocator, root: HKEY, sub: []const u8, name: []const u8) void { - const hk = openKey(alloc, root, sub, true) orelse return; - defer _ = RegCloseKey(hk); - const name_w = toW(alloc, name) catch return; - defer alloc.free(name_w); - _ = RegDeleteValueW(hk, name_w.ptr); -} - -const RegString = struct { value: []u8, reg_type: DWORD }; - -/// Read a REG_SZ / REG_EXPAND_SZ value. Caller owns the returned .value slice. -fn readRegString(alloc: std.mem.Allocator, hk: HKEY, name: []const u8) !RegString { - const name_w = try toW(alloc, name); - defer alloc.free(name_w); - - var reg_type: DWORD = REG_SZ; - var size_bytes: DWORD = 0; - // Query size and type (lpData = null is allowed). - _ = RegQueryValueExW(hk, name_w.ptr, null, ®_type, null, &size_bytes); - if (size_bytes == 0) return .{ .value = try alloc.dupe(u8, ""), .reg_type = reg_type }; - - // Allocate aligned u16 buffer and read the data. - const wchar_count = (size_bytes + 1) / 2; - const buf = try alloc.alloc(u16, wchar_count); - defer alloc.free(buf); - - var actual: DWORD = size_bytes; - if (RegQueryValueExW(hk, name_w.ptr, null, null, @ptrCast(buf.ptr), &actual) != ERROR_SUCCESS) - return error.ReadFailed; - - // Strip trailing null terminators before converting. - var len = actual / 2; - while (len > 0 and buf[len - 1] == 0) len -= 1; - - const utf8 = try std.unicode.utf16LeToUtf8Alloc(alloc, buf[0..len]); - return .{ .value = utf8, .reg_type = reg_type }; -} - -/// Write a string value back, preserving the original REG type. Errors are ignored. -fn writeRegString(alloc: std.mem.Allocator, hk: HKEY, name: []const u8, value: []const u8, reg_type: DWORD) void { - const name_w = toW(alloc, name) catch return; - defer alloc.free(name_w); - const val_w = toW(alloc, value) catch return; - defer alloc.free(val_w); - // Byte count includes the null terminator. - const byte_size: DWORD = @intCast((val_w.len + 1) * 2); - _ = RegSetValueExW(hk, name_w.ptr, 0, reg_type, @as([*]const u8, @ptrCast(val_w.ptr)), byte_size); -} - -// ── Core cleanup operations ─────────────────────────────────────────────────── - -/// Remove the old v1 symlink/junction directory (only if it is a reparse point). -fn removeJunction(alloc: std.mem.Allocator, path: []const u8) void { - const path_w = toW(alloc, path) catch return; - defer alloc.free(path_w); - const attrs = GetFileAttributesW(path_w.ptr); - if (attrs == INVALID_FILE_ATTRIBUTES) return; // does not exist β€” nothing to do - if (attrs & FILE_ATTRIBUTE_REPARSE_POINT == 0) { - logError("Symlink path exists but is not a junction/symlink, skipping removal: {s}", .{path}); - return; - } - if (RemoveDirectoryW(path_w.ptr) == 0) { - logError("Failed to remove junction/symlink: {s}", .{path}); - } -} - -/// Return true if a PATH segment should be removed during v1 cleanup. -/// Removes segments that: -/// - expand to nothing (empty/whitespace) -/// - contain %NVM_HOME% or %NVM_SYMLINK% references (case-insensitive) -/// - match install_norm or symlink_norm exactly (case-insensitive, normalized) -fn shouldRemoveSegment( - alloc: std.mem.Allocator, - seg: []const u8, - install_norm: ?[]const u8, - symlink_norm: ?[]const u8, -) bool { - const trimmed = std.mem.trim(u8, seg, " \t"); - if (trimmed.len == 0) return true; - const seg_norm = normPath(alloc, trimmed) catch return false; - defer alloc.free(seg_norm); - if (std.mem.indexOf(u8, seg_norm, "%nvm_home%") != null) return true; - if (std.mem.indexOf(u8, seg_norm, "%nvm_symlink%") != null) return true; - if (install_norm) |n| if (std.mem.eql(u8, seg_norm, n)) return true; - if (symlink_norm) |n| if (std.mem.eql(u8, seg_norm, n)) return true; - return false; -} - -/// Remove v1 NVM entries from the "Path" value in the given registry key. -fn cleanPath( - alloc: std.mem.Allocator, - root: HKEY, - sub: []const u8, - install_norm: ?[]const u8, - symlink_norm: ?[]const u8, -) void { - const hk = openKey(alloc, root, sub, true) orelse return; - defer _ = RegCloseKey(hk); - - const rs = readRegString(alloc, hk, "Path") catch return; - defer alloc.free(rs.value); - - var kept: std.ArrayListUnmanaged([]const u8) = .{}; - defer kept.deinit(alloc); - - var it = std.mem.splitScalar(u8, rs.value, ';'); - while (it.next()) |seg| { - if (!shouldRemoveSegment(alloc, seg, install_norm, symlink_norm)) - kept.append(alloc, seg) catch {}; - } - - const new_path = std.mem.join(alloc, ";", kept.items) catch return; - defer alloc.free(new_path); - - if (!std.mem.eql(u8, new_path, rs.value)) - writeRegString(alloc, hk, "Path", new_path, rs.reg_type); -} - -/// Read a string value from a subkey beneath an already-opened key. -/// Used to inspect UninstallString / DisplayIcon for uninstall-entry matching. -fn readSubkeyString(alloc: std.mem.Allocator, parent_hk: HKEY, subkey_name: []const u8, value_name: []const u8) ?[]u8 { - const sub_w = toW(alloc, subkey_name) catch return null; - defer alloc.free(sub_w); - var sub_hk: HKEY = undefined; - if (RegOpenKeyExW(parent_hk, sub_w.ptr, 0, KEY_READ, &sub_hk) != ERROR_SUCCESS) return null; - defer _ = RegCloseKey(sub_hk); - const rs = readRegString(alloc, sub_hk, value_name) catch return null; - return rs.value; -} - -/// Return true if the given uninstall subkey's UninstallString or DisplayIcon -/// contains install_norm as a substring (case-insensitive, backslash-normalised). -fn uninstallEntryMatchesDir( - alloc: std.mem.Allocator, - parent_hk: HKEY, - subkey_name: []const u8, - install_norm: []const u8, -) bool { - for ([_][]const u8{ "UninstallString", "DisplayIcon" }) |field| { - const raw = readSubkeyString(alloc, parent_hk, subkey_name, field) orelse continue; - defer alloc.free(raw); - const norm = normPath(alloc, raw) catch continue; - defer alloc.free(norm); - if (std.mem.indexOf(u8, norm, install_norm) != null) return true; - } - return false; -} - -/// Attempt to delete one uninstall key (key_name under uninstall_base) from root. -fn tryDeleteUninstallKey( - alloc: std.mem.Allocator, - root: HKEY, - uninstall_base: []const u8, - key_name: []const u8, -) void { - const full = std.fmt.allocPrint(alloc, "{s}\\{s}", .{ uninstall_base, key_name }) catch return; - defer alloc.free(full); - const full_w = toW(alloc, full) catch return; - defer alloc.free(full_w); - _ = SHDeleteKeyW(root, full_w.ptr); -} - -/// Remove nvm v1 uninstall entries from one hive + subkey combination. -fn removeUninstallFromHive( - alloc: std.mem.Allocator, - root: HKEY, - base: []const u8, - install_norm: ?[]const u8, -) void { - // Always attempt the canonical nvm_is1 key name directly. - tryDeleteUninstallKey(alloc, root, base, "nvm_is1"); - - // If we know the old install dir, also scan for any other matching entries. - const idir = install_norm orelse return; - const base_hk = openKey(alloc, root, base, false) orelse return; - defer _ = RegCloseKey(base_hk); - - // Snapshot subkey names before deleting (enumeration invalidates indices). - var names: std.ArrayListUnmanaged([]u8) = .{}; - defer { - for (names.items) |n| alloc.free(n); - names.deinit(alloc); - } - - var idx: DWORD = 0; - var name_buf: [256]u16 = undefined; - while (true) { - var name_len: DWORD = @intCast(name_buf.len); - const rc = RegEnumKeyExW(base_hk, idx, &name_buf, &name_len, null, null, null, null); - if (rc == ERROR_NO_MORE_ITEMS) break; - if (rc != ERROR_SUCCESS) break; - const name_utf8 = std.unicode.utf16LeToUtf8Alloc(alloc, name_buf[0..name_len]) catch { - idx += 1; - continue; - }; - names.append(alloc, name_utf8) catch { - alloc.free(name_utf8); - }; - idx += 1; - } - - for (names.items) |name| { - // nvm_is1 already handled above. - if (std.ascii.eqlIgnoreCase(name, "nvm_is1")) continue; - if (uninstallEntryMatchesDir(alloc, base_hk, name, idir)) - tryDeleteUninstallKey(alloc, root, base, name); - } -} - -/// Remove v1 uninstall registry entries from HKCU, HKLM, and HKLM\WOW6432Node. -fn removeUninstallEntries(alloc: std.mem.Allocator, install_norm: ?[]const u8) void { - const base = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; - const base_wow = "Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; - removeUninstallFromHive(alloc, windows.HKEY_CURRENT_USER, base, install_norm); - removeUninstallFromHive(alloc, windows.HKEY_LOCAL_MACHINE, base, install_norm); - removeUninstallFromHive(alloc, windows.HKEY_LOCAL_MACHINE, base_wow, install_norm); -} - -/// Recursively delete a directory tree. -fn removeTree(path: []const u8) void { - std.fs.deleteTreeAbsolute(path) catch |err| { - logError("Failed to remove directory tree '{s}': {}", .{ path, err }); - }; -} - -fn fileExistsAbsolute(path: []const u8) bool { - std.fs.accessAbsolute(path, .{}) catch return false; - return true; -} - -fn normalizeNodeExecutableInDir(alloc: std.mem.Allocator, install_path: []const u8) void { - const node_path = std.fmt.allocPrint(alloc, "{s}\\node.exe", .{install_path}) catch return; - defer alloc.free(node_path); - const node64_path = std.fmt.allocPrint(alloc, "{s}\\node64.exe", .{install_path}) catch return; - defer alloc.free(node64_path); - const node32_path = std.fmt.allocPrint(alloc, "{s}\\node32.exe", .{install_path}) catch return; - defer alloc.free(node32_path); - - const has_node = fileExistsAbsolute(node_path); - const has_node64 = fileExistsAbsolute(node64_path); - const has_node32 = fileExistsAbsolute(node32_path); - - if (has_node and has_node64) { - std.fs.deleteFileAbsolute(node_path) catch |err| { - logError("Failed to delete existing node.exe before promoting node64.exe in '{s}': {}", .{ install_path, err }); - return; - }; - std.fs.renameAbsolute(node64_path, node_path) catch |err| { - logError("Failed to rename node64.exe to node.exe in '{s}': {}", .{ install_path, err }); - return; - }; - logMsg("Promoted node64.exe to node.exe in '{s}'", .{install_path}); - } else if (!has_node and has_node64) { - std.fs.renameAbsolute(node64_path, node_path) catch |err| { - logError("Failed to rename node64.exe to node.exe in '{s}': {}", .{ install_path, err }); - return; - }; - logMsg("Renamed node64.exe to node.exe in '{s}'", .{install_path}); - } - - if (fileExistsAbsolute(node_path) and has_node32) { - std.fs.deleteFileAbsolute(node32_path) catch |err| { - logError("Failed to delete node32.exe in '{s}': {}", .{ install_path, err }); - return; - }; - logMsg("Removed node32.exe in '{s}'", .{install_path}); - } -} - -fn normalizeInstalledNodeExecutables(alloc: std.mem.Allocator, nvm_path: []const u8) void { - const parent = std.fs.path.dirname(nvm_path) orelse { - logError("normalize-installed-node-executables: could not determine parent directory of: {s}", .{nvm_path}); - return; - }; - const installs_dir = std.fmt.allocPrint(alloc, "{s}\\installs", .{parent}) catch return; - defer alloc.free(installs_dir); - - var dir = std.fs.openDirAbsolute(installs_dir, .{ .iterate = true }) catch |err| { - logError("Failed to open installs directory '{s}': {}", .{ installs_dir, err }); - return; - }; - defer dir.close(); - - var iter = dir.iterate(); - while (iter.next() catch |err| { - logError("Failed to enumerate installs directory '{s}': {}", .{ installs_dir, err }); - return; - }) |entry| { - if (entry.kind != .directory) continue; - const install_path = std.fmt.allocPrint(alloc, "{s}\\{s}", .{ installs_dir, entry.name }) catch continue; - defer alloc.free(install_path); - normalizeNodeExecutableInDir(alloc, install_path); - } -} - -/// Launch `nvm_path --register-eventlog` with no console window and wait for exit. -fn runEventLogRegistration(alloc: std.mem.Allocator, nvm_path: []const u8) void { - const cmd = std.fmt.allocPrint(alloc, "\"{s}\" --register-eventlog", .{nvm_path}) catch return; - defer alloc.free(cmd); - // CreateProcessW may modify the command-line buffer; use a mutable copy. - const cmd_w = toW(alloc, cmd) catch return; - defer alloc.free(cmd_w); - - var si = std.mem.zeroes(STARTUPINFOW); - si.cb = @sizeOf(STARTUPINFOW); - var pi = std.mem.zeroes(PROCESS_INFORMATION); - - if (CreateProcessW(null, cmd_w.ptr, null, null, 0, CREATE_NO_WINDOW, null, null, &si, &pi) == 0) { - logError("Failed to launch nvm --register-eventlog: {s}", .{nvm_path}); - return; - } - _ = WaitForSingleObject(pi.hProcess, INFINITE); - _ = CloseHandle(pi.hProcess); - _ = CloseHandle(pi.hThread); -} - -/// Launch `reshim.exe --force` from the utils\ sibling directory of nvm.exe. -fn reshim(alloc: std.mem.Allocator, nvm_path: []const u8) void { - // Derive the utils\ dir from the nvm.exe path: strip the filename, append utils\reshim.exe. - const parent = std.fs.path.dirname(nvm_path) orelse { - logError("reshim: could not determine parent directory of: {s}", .{nvm_path}); - return; - }; - const reshim_path = std.fmt.allocPrint(alloc, "{s}\\utils\\reshim.exe", .{parent}) catch return; - defer alloc.free(reshim_path); - - const cmd = std.fmt.allocPrint(alloc, "\"{s}\" --force", .{reshim_path}) catch return; - defer alloc.free(cmd); - const cmd_w = toW(alloc, cmd) catch return; - defer alloc.free(cmd_w); - - var si = std.mem.zeroes(STARTUPINFOW); - si.cb = @sizeOf(STARTUPINFOW); - var pi = std.mem.zeroes(PROCESS_INFORMATION); - - if (CreateProcessW(null, cmd_w.ptr, null, null, 0, CREATE_NO_WINDOW, null, null, &si, &pi) == 0) { - logError("reshim: failed to launch reshim.exe --force: {s}", .{reshim_path}); - return; - } - _ = WaitForSingleObject(pi.hProcess, INFINITE); - _ = CloseHandle(pi.hProcess); - _ = CloseHandle(pi.hThread); -} - -// ── Entry point ─────────────────────────────────────────────────────────────── - -pub fn main() void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const alloc = gpa.allocator(); - - // Initialise the global log state. - g_log_alloc = alloc; - defer g_log_buf.deinit(alloc); - - const args = std.process.argsAlloc(alloc) catch return; - defer std.process.argsFree(alloc, args); - - var install_dir: ?[]const u8 = null; - var symlink_path: ?[]const u8 = null; - var new_nvm: ?[]const u8 = null; - - var i: usize = 1; - while (i < args.len) : (i += 1) { - if (std.mem.eql(u8, args[i], "--install-dir") and i + 1 < args.len) { - i += 1; - install_dir = args[i]; - } else if (std.mem.eql(u8, args[i], "--symlink-path") and i + 1 < args.len) { - i += 1; - symlink_path = args[i]; - } else if (std.mem.eql(u8, args[i], "--new-nvm") and i + 1 < args.len) { - i += 1; - new_nvm = args[i]; - } - } - - // Log header with Unix timestamp and invocation arguments. - logMsg("=== nvm-upgrader v1->v2 migration log (ts:{d}) ===", .{std.time.timestamp()}); - logMsg(" --install-dir : {s}", .{install_dir orelse "(not provided)"}); - logMsg(" --symlink-path : {s}", .{symlink_path orelse "(not provided)"}); - logMsg(" --new-nvm : {s}", .{new_nvm orelse "(not provided)"}); - logMsg("", .{}); - - // Pre-compute normalized (lowercase, no trailing backslash) paths for comparisons. - const install_norm: ?[]u8 = if (install_dir) |d| normPath(alloc, d) catch null else null; - defer if (install_norm) |n| alloc.free(n); - const symlink_norm: ?[]u8 = if (symlink_path) |s| normPath(alloc, s) catch null else null; - defer if (symlink_norm) |n| alloc.free(n); - - const user_env = "Environment"; - const system_env = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; - - // 1. Remove the v1 symlink/junction directory. - logMsg("[STEP 1] Remove v1 symlink/junction", .{}); - if (symlink_path) |sp| removeJunction(alloc, sp); - - // 2–3. Delete NVM_SYMLINK from both user and system environment. - logMsg("[STEP 2] Delete NVM_SYMLINK registry values", .{}); - deleteRegValue(alloc, windows.HKEY_CURRENT_USER, user_env, "NVM_SYMLINK"); - deleteRegValue(alloc, windows.HKEY_LOCAL_MACHINE, system_env, "NVM_SYMLINK"); - - // 4–5. Delete NVM_HOME from both user and system environment. - // The installer's EnsureNvmPathPriority re-creates HKCU NVM_HOME for v2 immediately after. - logMsg("[STEP 3] Delete NVM_HOME registry values", .{}); - deleteRegValue(alloc, windows.HKEY_CURRENT_USER, user_env, "NVM_HOME"); - deleteRegValue(alloc, windows.HKEY_LOCAL_MACHINE, system_env, "NVM_HOME"); - - // 6–7. Strip v1 path entries from user and system PATH values. - logMsg("[STEP 4] Clean v1 entries from PATH", .{}); - cleanPath(alloc, windows.HKEY_CURRENT_USER, user_env, install_norm, symlink_norm); - cleanPath(alloc, windows.HKEY_LOCAL_MACHINE, system_env, install_norm, symlink_norm); - - // 8. Remove the v1 "installed application" uninstall registry entries. - logMsg("[STEP 5] Remove v1 uninstall registry entries", .{}); - removeUninstallEntries(alloc, install_norm); - - // 9. Remove the old v1 installation directory (e.g. %AppData%\Roaming\nvm). - logMsg("[STEP 6] Remove v1 installation directory", .{}); - if (install_dir) |dir| removeTree(dir); - - // 10. Register the Windows Event Log source in the new nvm.exe binary. - logMsg("[STEP 7] Register Windows Event Log source", .{}); - if (new_nvm) |nvm| runEventLogRegistration(alloc, nvm); - - // 11. Normalize legacy node*.exe names inside each installed Node version directory. - logMsg("[STEP 8] Normalize installed node executables", .{}); - if (new_nvm) |nvm| normalizeInstalledNodeExecutables(alloc, nvm); - - // 12. Rebuild the .shim hardlinks for every installed Node version. - logMsg("[STEP 9] Rebuild module shims (reshim --force)", .{}); - if (new_nvm) |nvm| reshim(alloc, nvm); - - logMsg("", .{}); - if (g_has_errors) { - logMsg("Migration completed with errors (see above).", .{}); - } else { - logMsg("Migration completed successfully.", .{}); - } - - // Write upgrade.log to install_dir only when errors were recorded. - if (install_dir) |dir| flushUpgradeLog(dir); -} diff --git a/installer/updater/winres/winres.json b/installer/updater/winres/winres.json deleted file mode 100644 index 73c75506..00000000 --- a/installer/updater/winres/winres.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "RT_MANIFEST": { - "#1": { - "0409": { - "identity": { - "name": "NVM for Windows Upgrader", - "version": "1.0.0" - }, - "description": "NVM for Windows v1 to v2 migration helper", - "minimum-os": "win10", - "execution-level": "require administrator", - "ui-access": false, - "auto-elevate": false, - "dpi-awareness": "system", - "disable-theming": false, - "disable-window-filtering": false, - "high-resolution-scrolling-aware": false, - "ultra-high-resolution-scrolling-aware": false, - "long-path-aware": false, - "printer-driver-isolation": false, - "gdi-scaling": false, - "segment-heap": false, - "use-common-controls-v6": false - } - } - } -} diff --git a/nvm.iss b/nvm.iss new file mode 100644 index 00000000..40d4642b --- /dev/null +++ b/nvm.iss @@ -0,0 +1,316 @@ +#define MyAppName "NVM for Windows" +#define MyAppShortName "nvm" +#define MyAppLCShortName "nvm" +#define MyAppVersion "1.1.1" +#define MyAppPublisher "Ecor Ventures LLC" +#define MyAppURL "http://github.com/coreybutler/nvm" +#define MyAppExeName "nvm.exe" +#define MyIcon "bin\nodejs.ico" +#define ProjectRoot "C:\Users\Corey\Documents\workspace\nvm-windows" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +PrivilegesRequired=admin +AppId=40078385-F676-4C61-9A9C-F9028599D6D3 +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={userappdata}\{#MyAppShortName} +DisableDirPage=no +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +LicenseFile={#ProjectRoot}\LICENSE +OutputDir={#ProjectRoot}\dist\{#MyAppVersion} +OutputBaseFilename={#MyAppLCShortName}-setup +SetupIconFile={#ProjectRoot}\{#MyIcon} +Compression=lzma +SolidCompression=yes +ChangesEnvironment=yes +DisableProgramGroupPage=yes +ArchitecturesInstallIn64BitMode=x64 ia64 +UninstallDisplayIcon={app}\{#MyIcon} +AppCopyright=Copyright (C) 2016 Corey Butler and contributors. + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 + +[Files] +Source: "{#ProjectRoot}\bin\*"; DestDir: "{app}"; BeforeInstall: PreInstall; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "{#ProjectRoot}\bin\install.cmd" + +[Icons] +Name: "{group}\{#MyAppShortName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{#MyIcon}" +Name: "{group}\Uninstall {#MyAppShortName}"; Filename: "{uninstallexe}" + +[Code] +var + SymlinkPage: TInputDirWizardPage; + +function IsDirEmpty(dir: string): Boolean; +var + FindRec: TFindRec; + ct: Integer; +begin + ct := 0; + if FindFirst(ExpandConstant(dir + '\*'), FindRec) then + try + repeat + if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then + ct := ct+1; + until + not FindNext(FindRec); + finally + FindClose(FindRec); + Result := ct = 0; + end; +end; + +//function getInstalledVErsions(dir: string): +var + nodeInUse: string; + +function TakeControl(np: string; nv: string): string; +var + path: string; +begin + // Move the existing node.js installation directory to the nvm root & update the path + RenameFile(np,ExpandConstant('{app}')+'\'+nv); + + RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', path); + + StringChangeEx(path,np+'\','',True); + StringChangeEx(path,np,'',True); + StringChangeEx(path,np+';;',';',True); + + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); + + RegQueryStringValue(HKEY_CURRENT_USER, + 'Environment', + 'Path', path); + + StringChangeEx(path,np+'\','',True); + StringChangeEx(path,np,'',True); + StringChangeEx(path,np+';;',';',True); + + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); + + nodeInUse := ExpandConstant('{app}')+'\'+nv; + +end; + +function Ansi2String(AString:AnsiString):String; +var + i : Integer; + iChar : Integer; + outString : String; +begin + outString :=''; + for i := 1 to Length(AString) do + begin + iChar := Ord(AString[i]); //get int value + outString := outString + Chr(iChar); + end; + + Result := outString; +end; + +procedure PreInstall(); +var + TmpResultFile, TmpJS, NodeVersion, NodePath: string; + stdout: Ansistring; + ResultCode: integer; + msg1, msg2, msg3, dir1: Boolean; +begin + // Create a file to check for Node.JS + TmpJS := ExpandConstant('{tmp}') + '\nvm_check.js'; + SaveStringToFile(TmpJS, 'console.log(require("path").dirname(process.execPath));', False); + + // Execute the node file and save the output temporarily + TmpResultFile := ExpandConstant('{tmp}') + '\nvm_node_check.txt'; + Exec(ExpandConstant('{cmd}'), '/C node "'+TmpJS+'" > "' + TmpResultFile + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + DeleteFile(TmpJS) + + // Process the results + LoadStringFromFile(TmpResultFile,stdout); + NodePath := Trim(Ansi2String(stdout)); + if DirExists(NodePath) then begin + Exec(ExpandConstant('{cmd}'), '/C node -v > "' + TmpResultFile + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + LoadStringFromFile(TmpResultFile, stdout); + NodeVersion := Trim(Ansi2String(stdout)); + msg1 := MsgBox('Node '+NodeVersion+' is already installed. Do you want NVM to control this version?', mbConfirmation, MB_YESNO) = IDNO; + if msg1 then begin + msg2 := MsgBox('NVM cannot run in parallel with an existing Node.js installation. Node.js must be uninstalled before NVM can be installed, or you must allow NVM to control the existing installation. Do you want NVM to control node '+NodeVersion+'?', mbConfirmation, MB_YESNO) = IDYES; + if msg2 then begin + TakeControl(NodePath, NodeVersion); + end; + if not msg2 then begin + DeleteFile(TmpResultFile); + WizardForm.Close; + end; + end; + if not msg1 then + begin + TakeControl(NodePath, NodeVersion); + end; + end; + + // Make sure the symlink directory doesn't exist + if DirExists(SymlinkPage.Values[0]) then begin + // If the directory is empty, just delete it since it will be recreated anyway. + dir1 := IsDirEmpty(SymlinkPage.Values[0]); + if dir1 then begin + RemoveDir(SymlinkPage.Values[0]); + end; + if not dir1 then begin + msg3 := MsgBox(SymlinkPage.Values[0]+' will be overwritten and all contents will be lost. Do you want to proceed?', mbConfirmation, MB_OKCANCEL) = IDOK; + if msg3 then begin + RemoveDir(SymlinkPage.Values[0]); + end; + if not msg3 then begin + //RaiseException('The symlink cannot be created due to a conflict with the existing directory at '+SymlinkPage.Values[0]); + WizardForm.Close; + end; + end; + end; +end; + +procedure InitializeWizard; +begin + SymlinkPage := CreateInputDirPage(wpSelectDir, + 'Set Node.js Symlink', 'The active version of Node.js will always be available here.', + 'Select the folder in which Setup should create the symlink, then click Next.', + False, ''); + SymlinkPage.Add('This directory will automatically be added to your system path.'); + SymlinkPage.Values[0] := ExpandConstant('{pf}\nodejs'); +end; + +function InitializeUninstall(): Boolean; +var + path: string; + nvm_symlink: string; +begin + MsgBox('Removing NVM for Windows will remove the nvm command and all versions of node.js, including global npm modules.', mbInformation, MB_OK); + + // Remove the symlink + RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'NVM_SYMLINK', nvm_symlink); + RemoveDir(nvm_symlink); + + // Clean the registry + RegDeleteValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'NVM_HOME') + RegDeleteValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'NVM_SYMLINK') + RegDeleteValue(HKEY_CURRENT_USER, + 'Environment', + 'NVM_HOME') + RegDeleteValue(HKEY_CURRENT_USER, + 'Environment', + 'NVM_SYMLINK') + + RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', path); + + StringChangeEx(path,'%NVM_HOME%','',True); + StringChangeEx(path,'%NVM_SYMLINK%','',True); + StringChangeEx(path,';;',';',True); + + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); + + RegQueryStringValue(HKEY_CURRENT_USER, + 'Environment', + 'Path', path); + + StringChangeEx(path,'%NVM_HOME%','',True); + StringChangeEx(path,'%NVM_SYMLINK%','',True); + StringChangeEx(path,';;',';',True); + + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); + + Result := True; +end; + +// Generate the settings file based on user input & update registry +procedure CurStepChanged(CurStep: TSetupStep); +var + path: string; +begin + if CurStep = ssPostInstall then + begin + SaveStringToFile(ExpandConstant('{app}\settings.txt'), 'root: ' + ExpandConstant('{app}') + #13#10 + 'path: ' + SymlinkPage.Values[0] + #13#10, False); + + // Add Registry settings + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_HOME', ExpandConstant('{app}')); + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_SYMLINK', SymlinkPage.Values[0]); + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'NVM_HOME', ExpandConstant('{app}')); + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'NVM_SYMLINK', SymlinkPage.Values[0]); + + // Update system and user PATH if needed + RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', path); + if Pos('%NVM_HOME%',path) = 0 then begin + path := path+';%NVM_HOME%'; + StringChangeEx(path,';;',';',True); + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); + end; + if Pos('%NVM_SYMLINK%',path) = 0 then begin + path := path+';%NVM_SYMLINK%'; + StringChangeEx(path,';;',';',True); + RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); + end; + RegQueryStringValue(HKEY_CURRENT_USER, + 'Environment', + 'Path', path); + if Pos('%NVM_HOME%',path) = 0 then begin + path := path+';%NVM_HOME%'; + StringChangeEx(path,';;',';',True); + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); + end; + if Pos('%NVM_SYMLINK%',path) = 0 then begin + path := path+';%NVM_SYMLINK%'; + StringChangeEx(path,';;',';',True); + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); + end; + end; +end; + +function getSymLink(o: string): string; +begin + Result := SymlinkPage.Values[0]; +end; + +function getCurrentVersion(o: string): string; +begin + Result := nodeInUse; +end; + +function isNodeAlreadyInUse(): boolean; +begin + Result := Length(nodeInUse) > 0; +end; + +[Run] +Filename: "{cmd}"; Parameters: "/C ""mklink /D ""{code:getSymLink}"" ""{code:getCurrentVersion}"""" "; Check: isNodeAlreadyInUse; Flags: runhidden; + +[UninstallDelete] +Type: files; Name: "{app}\nvm.exe"; +Type: files; Name: "{app}\elevate.cmd"; +Type: files; Name: "{app}\elevate.vbs"; +Type: files; Name: "{app}\nodejs.ico"; +Type: files; Name: "{app}\settings.txt"; +Type: filesandordirs; Name: "{app}"; diff --git a/shim b/shim deleted file mode 160000 index 26c53f69..00000000 --- a/shim +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 26c53f69b9a61e871553f4b27058ba1687f788b3 diff --git a/src/nvm.go b/src/nvm.go new file mode 100644 index 00000000..ffe3fe52 --- /dev/null +++ b/src/nvm.go @@ -0,0 +1,601 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + "io/ioutil" + "regexp" + "bytes" + "./nvm/web" + "./nvm/arch" + "./nvm/file" + "./nvm/node" + "github.com/olekukonko/tablewriter" +) + +const ( + NvmVersion = "1.1.1" +) + +type Environment struct { + settings string + root string + symlink string + arch string + node_mirror string + npm_mirror string + proxy string + originalpath string + originalversion string +} + +var env = &Environment{ + settings: os.Getenv("NVM_HOME")+"\\settings.txt", + root: "", + symlink: os.Getenv("NVM_SYMLINK"), + arch: os.Getenv("PROCESSOR_ARCHITECTURE"), + node_mirror: "", + npm_mirror: "", + proxy: "none", + originalpath: "", + originalversion: "", +} + +func main() { + args := os.Args + detail := "" + procarch := arch.Validate(env.arch) + + Setup() + + // Capture any additional arguments + if len(args) > 2 { + detail = args[2] + } + if len(args) > 3 { + procarch = args[3] + } + if len(args) < 2 { + help() + return + } + + // Run the appropriate method + switch args[1] { + case "install": install(detail,procarch) + case "uninstall": uninstall(detail) + case "use": use(detail,procarch) + case "list": list(detail) + case "ls": list(detail) + case "on": enable() + case "off": disable() + case "root": + if len(args) == 3 { + updateRootDir(args[2]) + } else { + fmt.Println("\nCurrent Root: "+env.root) + } + case "version": + fmt.Println(NvmVersion) + case "v": + fmt.Println(NvmVersion) + case "arch": + if strings.Trim(detail," \r\n") != "" { + detail = strings.Trim(detail," \r\n") + if detail != "32" && detail != "64" { + fmt.Println("\""+detail+"\" is an invalid architecture. Use 32 or 64.") + return + } + env.arch = detail + saveSettings() + fmt.Println("Default architecture set to "+detail+"-bit.") + return + } + _, a := node.GetCurrentVersion() + fmt.Println("System Default: "+env.arch+"-bit.") + fmt.Println("Currently Configured: "+a+"-bit.") + case "proxy": + if detail == "" { + fmt.Println("Current proxy: "+env.proxy) + } else { + env.proxy = detail + saveSettings() + } + case "update": update() + default: help() + } +} + +func update() { +// cmd := exec.Command("cmd", "/d", "echo", "testing") +// var output bytes.Buffer +// var _stderr bytes.Buffer +// cmd.Stdout = &output +// cmd.Stderr = &_stderr +// perr := cmd.Run() +// if perr != nil { +// fmt.Println(fmt.Sprint(perr) + ": " + _stderr.String()) +// return +// } +} + +func CheckVersionExceedsLatest(version string) bool{ + //content := web.GetRemoteTextFile("http://nodejs.org/dist/latest/SHASUMS256.txt") + url := web.GetFullNodeUrl("latest/SHASUMS256.txt"); + content := web.GetRemoteTextFile(url) + re := regexp.MustCompile("node-v(.+)+msi") + reg := regexp.MustCompile("node-v|-x.+") + latest := reg.ReplaceAllString(re.FindString(content),"") + + if version <= latest { + return false + } else { + return true + } +} + +func install(version string, cpuarch string) { + if version == "" { + fmt.Println("\nInvalid version.") + fmt.Println(" ") + help() + return + } + + cpuarch = strings.ToLower(cpuarch) + + if cpuarch != "" { + if cpuarch != "32" && cpuarch != "64" && cpuarch != "all" { + fmt.Println("\""+cpuarch+"\" is not a valid CPU architecture. Must be 32 or 64.") + return + } + } else { + cpuarch = env.arch + } + + if cpuarch != "all" { + cpuarch = arch.Validate(cpuarch) + } + + // If user specifies "latest" version, find out what version is + if version == "latest" { + url := web.GetFullNodeUrl("latest/SHASUMS256.txt"); + content := web.GetRemoteTextFile(url) + re := regexp.MustCompile("node-v(.+)+msi") + reg := regexp.MustCompile("node-v|-x.+") + version = reg.ReplaceAllString(re.FindString(content),"") + } + + version = cleanVersion(version) + + if CheckVersionExceedsLatest(version) { + fmt.Println("Node.js v"+version+" is not yet released or available.") + return + } + + if cpuarch == "64" && !web.IsNode64bitAvailable(version) { + fmt.Println("Node.js v"+version+" is only available in 32-bit.") + return + } + + // Check to see if the version is already installed + if !node.IsVersionInstalled(env.root,version,cpuarch) { + + if !node.IsVersionAvailable(version){ + fmt.Println("Version "+version+" is not available. If you are attempting to download a \"just released\" version,") + fmt.Println("it may not be recognized by the nvm service yet (updated hourly). If you feel this is in error and") + fmt.Println("you know the version exists, please visit http://github.com/coreybutler/nodedistro and submit a PR.") + return + } + + // Make the output directories + os.Mkdir(env.root+"\\v"+version,os.ModeDir) + os.Mkdir(env.root+"\\v"+version+"\\node_modules",os.ModeDir) + + // Download node + if (cpuarch == "32" || cpuarch == "all") && !node.IsVersionInstalled(env.root,version,"32") { + success := web.GetNodeJS(env.root,version,"32"); + if !success { + os.RemoveAll(env.root+"\\v"+version+"\\node_modules") + fmt.Println("Could not download node.js v"+version+" 32-bit executable.") + return + } + } + if (cpuarch == "64" || cpuarch == "all") && !node.IsVersionInstalled(env.root,version,"64") { + success := web.GetNodeJS(env.root,version,"64"); + if !success { + os.RemoveAll(env.root+"\\v"+version+"\\node_modules") + fmt.Println("Could not download node.js v"+version+" 64-bit executable.") + return + } + } + + if file.Exists(env.root+"\\v"+version+"\\node_modules\\npm") { + return + } + + // If successful, add npm + npmv := getNpmVersion(version) + success := web.GetNpm(env.root, getNpmVersion(version)) + if success { + fmt.Printf("Installing npm v"+npmv+"...") + + // new temp directory under the nvm root + tempDir := env.root + "\\temp" + + // Extract npm to the temp directory + file.Unzip(tempDir+"\\npm-v"+npmv+".zip",tempDir+"\\nvm-npm") + + // Copy the npm and npm.cmd files to the installation directory + os.Rename(tempDir+"\\nvm-npm\\npm-"+npmv+"\\bin\\npm",env.root+"\\v"+version+"\\npm") + os.Rename(tempDir+"\\nvm-npm\\npm-"+npmv+"\\bin\\npm.cmd",env.root+"\\v"+version+"\\npm.cmd") + os.Rename(tempDir+"\\nvm-npm\\npm-"+npmv,env.root+"\\v"+version+"\\node_modules\\npm") + + // Remove the temp directory + // may consider keep the temp files here + os.RemoveAll(tempDir) + + fmt.Println("\n\nInstallation complete. If you want to use this version, type\n\nnvm use "+version) + } else { + fmt.Println("Could not download npm for node v"+version+".") + fmt.Println("Please visit https://github.com/npm/npm/releases/tag/v"+npmv+" to download npm.") + fmt.Println("It should be extracted to "+env.root+"\\v"+version) + } + + // If this is ever shipped for Mac, it should use homebrew. + // If this ever ships on Linux, it should be on bintray so it can use yum, apt-get, etc. + + return + } else { + fmt.Println("Version "+version+" is already installed.") + return + } + +} + +func uninstall(version string) { + // Make sure a version is specified + if len(version) == 0 { + fmt.Println("Provide the version you want to uninstall.") + help() + return + } + + version = cleanVersion(version) + + // Determine if the version exists and skip if it doesn't + if node.IsVersionInstalled(env.root,version,"32") || node.IsVersionInstalled(env.root,version,"64") { + fmt.Printf("Uninstalling node v"+version+"...") + v, _ := node.GetCurrentVersion() + if v == version { + cmd := exec.Command(env.root+"\\elevate.cmd", "cmd", "/C", "rmdir", env.symlink) + cmd.Run() + } + e := os.RemoveAll(env.root+"\\v"+version) + if e != nil { + fmt.Println("Error removing node v"+version) + fmt.Println("Manually remove "+env.root+"\\v"+version+".") + } else { + fmt.Printf(" done") + } + } else { + fmt.Println("node v"+version+" is not installed. Type \"nvm list\" to see what is installed.") + } + return +} + +func cleanVersion(version string) string { + re := regexp.MustCompile("\\d+.\\d+.\\d+") + matched := re.FindString(version) + + if len(matched) == 0 { + re = regexp.MustCompile("\\d+.\\d+") + matched = re.FindString(version) + if len(matched) == 0 { + matched = version + ".0.0" + } else { + matched = matched + ".0" + } + fmt.Println(matched) + } + + return matched +} + +func use(version string, cpuarch string) { + if version == "32" || version == "64" { + cpuarch = version + v, _ := node.GetCurrentVersion() + version = v + } + + cpuarch = arch.Validate(cpuarch) + + version = cleanVersion(version) + + // Make sure the version is installed. If not, warn. + if !node.IsVersionInstalled(env.root,version,cpuarch) { + fmt.Println("node v"+version+" ("+cpuarch+"-bit) is not installed.") + if cpuarch == "32" { + if node.IsVersionInstalled(env.root,version,"64") { + fmt.Println("\nDid you mean node v"+version+" (64-bit)?\nIf so, type \"nvm use "+version+" 64\" to use it.") + } + } + if cpuarch == "64" { + if node.IsVersionInstalled(env.root,version,"64") { + fmt.Println("\nDid you mean node v"+version+" (64-bit)?\nIf so, type \"nvm use "+version+" 64\" to use it.") + } + } + return + } + + // Create or update the symlink + sym, _ := os.Stat(env.symlink) + if sym != nil { + cmd := exec.Command(env.root+"\\elevate.cmd", "cmd", "/C", "rmdir", env.symlink) + var output bytes.Buffer + var _stderr bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &_stderr + perr := cmd.Run() + if perr != nil { + fmt.Println(fmt.Sprint(perr) + ": " + _stderr.String()) + return + } + } + + c := exec.Command(env.root+"\\elevate.cmd", "cmd", "/C", "mklink", "/D", env.symlink, env.root+"\\v"+version) + var out bytes.Buffer + var stderr bytes.Buffer + c.Stdout = &out + c.Stderr = &stderr + err := c.Run() + if err != nil { + fmt.Println(fmt.Sprint(err) + ": " + stderr.String()) + return + } + + // Use the assigned CPU architecture + cpuarch = arch.Validate(cpuarch) + e32 := file.Exists(env.root+"\\v"+version+"\\node32.exe") + e64 := file.Exists(env.root+"\\v"+version+"\\node64.exe") + used := file.Exists(env.root+"\\v"+version+"\\node.exe") + if (e32 || e64) { + if used { + if e32 { + os.Rename(env.root+"\\v"+version+"\\node.exe",env.root+"\\v"+version+"\\node64.exe") + os.Rename(env.root+"\\v"+version+"\\node32.exe",env.root+"\\v"+version+"\\node.exe") + } else { + os.Rename(env.root+"\\v"+version+"\\node.exe",env.root+"\\v"+version+"\\node32.exe") + os.Rename(env.root+"\\v"+version+"\\node64.exe",env.root+"\\v"+version+"\\node.exe") + } + } else if e32 || e64 { + os.Rename(env.root+"\\v"+version+"\\node"+cpuarch+".exe",env.root+"\\v"+version+"\\node.exe") + } + } + fmt.Println("Now using node v"+version+" ("+cpuarch+"-bit)") +} + +func useArchitecture(a string) { + if strings.ContainsAny("32",os.Getenv("PROCESSOR_ARCHITECTURE")) { + fmt.Println("This computer only supports 32-bit processing.") + return + } + if a == "32" || a == "64" { + env.arch = a + saveSettings() + fmt.Println("Set to "+a+"-bit mode") + } else { + fmt.Println("Cannot set architecture to "+a+". Must be 32 or 64 are acceptable values.") + } +} + +func list(listtype string) { + if listtype == "" { + listtype = "installed" + } + if listtype != "installed" && listtype != "available" { + fmt.Println("\nInvalid list option.\n\nPlease use on of the following\n - nvm list\n - nvm list installed\n - nvm list available") + help() + return + } + + if listtype == "installed" { + fmt.Println("") + inuse, a := node.GetCurrentVersion() + + v := node.GetInstalled(env.root) + for i := 0; i < len(v); i++ { + version := v[i] + isnode, _ := regexp.MatchString("v",version) + str := "" + if isnode { + if "v"+inuse == version { + str = str+" * " + } else { + str = str+" " + } + str = str+regexp.MustCompile("v").ReplaceAllString(version,"") + if "v"+inuse == version { + str = str+" (Currently using "+a+"-bit executable)" +// str = ansi.Color(str,"green:black") + } + fmt.Printf(str+"\n") + } + } + if len(v) == 0 { + fmt.Println("No installations recognized.") + } + } else { + _, lts, current, stable, unstable, _ := node.GetAvailable() + + releases := 20 + + data := make([][]string, releases, releases + 5) + for i := 0; i < releases; i++ { + release := make([]string, 4, 6) + + release[0] = "" + release[1] = "" + release[2] = "" + release[3] = "" + + if len(current) > i { + if len(current[i]) > 0 { + release[0] = current[i] + } + } + + if len(lts) > i { + if len(lts[i]) > 0 { + release[1] = lts[i] + } + } + + if len(stable) > i { + if len(stable[i]) > 0 { + release[2] = stable[i] + } + } + + if len(unstable) > i { + if len(unstable[i]) > 0 { + release[3] = unstable[i] + } + } + + data[i] = release + } + + fmt.Println("") + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{" Current ", " LTS ", " Old Stable ", "Old Unstable"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetAlignment(tablewriter.ALIGN_CENTER) + table.SetCenterSeparator("|") + table.AppendBulk(data) // Add Bulk Data + table.Render() + + fmt.Println("\nThis is a partial list. For a complete list, visit https://nodejs.org/download/release") + } +} + +func enable() { + dir := "" + files, _ := ioutil.ReadDir(env.root) + for _, f := range files { + if f.IsDir() { + isnode, _ := regexp.MatchString("v",f.Name()) + if isnode { + dir = f.Name() + } + } + } + fmt.Println("nvm enabled") + if dir != "" { + use(strings.Trim(regexp.MustCompile("v").ReplaceAllString(dir,"")," \n\r"),env.arch) + } else { + fmt.Println("No versions of node.js found. Try installing the latest by typing nvm install latest") + } +} + +func disable() { + cmd := exec.Command(env.root+"\\elevate.cmd", "cmd", "/C", "rmdir", env.symlink) + cmd.Run() + fmt.Println("nvm disabled") +} + +func help() { + fmt.Println("\nRunning version "+NvmVersion+".") + fmt.Println("\nUsage:") + fmt.Println(" ") + fmt.Println(" nvm arch : Show if node is running in 32 or 64 bit mode.") + fmt.Println(" nvm install [arch] : The version can be a node.js version or \"latest\" for the latest stable version.") + fmt.Println(" Optionally specify whether to install the 32 or 64 bit version (defaults to system arch).") + fmt.Println(" Set [arch] to \"all\" to install 32 AND 64 bit versions.") + fmt.Println(" nvm list [available] : List the node.js installations. Type \"available\" at the end to see what can be installed. Aliased as ls.") + fmt.Println(" nvm on : Enable node.js version management.") + fmt.Println(" nvm off : Disable node.js version management.") + fmt.Println(" nvm proxy [url] : Set a proxy to use for downloads. Leave [url] blank to see the current proxy.") + fmt.Println(" Set [url] to \"none\" to remove the proxy.") + fmt.Println(" nvm node_mirror [url] : Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.") + fmt.Println(" nvm npm_mirror [url] : Set the npm mirror. Defaults to https://github.com/npm/npm/archive/. Leave [url] blank to default url.") + fmt.Println(" nvm uninstall : The version must be a specific version.") +// fmt.Println(" nvm update : Automatically update nvm to the latest version.") + fmt.Println(" nvm use [version] [arch] : Switch to use the specified version. Optionally specify 32/64bit architecture.") + fmt.Println(" nvm use will continue using the selected version, but switch to 32/64 bit mode.") + fmt.Println(" nvm root [path] : Set the directory where nvm should store different versions of node.js.") + fmt.Println(" If is not set, the current root will be displayed.") + fmt.Println(" nvm version : Displays the current running version of nvm for Windows. Aliased as v.") + fmt.Println(" ") +} + +// Given a node.js version, returns the associated npm version +func getNpmVersion(nodeversion string) string { + + _, _, _, _, _, npm := node.GetAvailable() + + return npm[nodeversion] +} + +func updateRootDir(path string) { + _, err := os.Stat(path) + if err != nil { + fmt.Println(path+" does not exist or could not be found.") + return + } + + env.root = path + saveSettings() + fmt.Println("\nRoot has been set to "+path) +} + +func saveSettings() { + content := "root: "+strings.Trim(env.root," \n\r")+"\r\narch: "+strings.Trim(env.arch," \n\r")+"\r\nproxy: "+strings.Trim(env.proxy," \n\r")+"\r\noriginalpath: "+strings.Trim(env.originalpath," \n\r")+"\r\noriginalversion: "+strings.Trim(env.originalversion," \n\r") + content = content + "node_mirror: "+strings.Trim(env.node_mirror," \n\r")+ "npm_mirror: "+strings.Trim(env.npm_mirror," \n\r") + ioutil.WriteFile(env.settings, []byte(content), 0644) +} + +func Setup() { + lines, err := file.ReadLines(env.settings) + if err != nil { + fmt.Println("\nERROR",err) + os.Exit(1) + } + + // Process each line and extract the value + for _, line := range lines { + if strings.Contains(line,"root:") { + env.root = strings.Trim(regexp.MustCompile("root:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line,"originalpath:") { + env.originalpath = strings.Trim(regexp.MustCompile("originalpath:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line,"originalversion:") { + env.originalversion = strings.Trim(regexp.MustCompile("originalversion:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line,"arch:"){ + env.arch = strings.Trim(regexp.MustCompile("arch:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line, "node_mirror:"){ + env.node_mirror = strings.Trim(regexp.MustCompile("node_mirror:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line, "npm_mirror:"){ + env.npm_mirror = strings.Trim(regexp.MustCompile("npm_mirror:").ReplaceAllString(line,"")," \r\n") + } else if strings.Contains(line,"proxy:"){ + env.proxy = strings.Trim(regexp.MustCompile("proxy:").ReplaceAllString(line,"")," \r\n") + if env.proxy != "none" && env.proxy != "" { + if strings.ToLower(env.proxy[0:4]) != "http" { + env.proxy = "http://"+env.proxy + } + web.SetProxy(env.proxy) + } + } + } + web.SetMirrors(env.node_mirror, env.npm_mirror) + env.arch = arch.Validate(env.arch) + + // Make sure the directories exist + _, e := os.Stat(env.root) + if e != nil { + fmt.Println(env.root+" could not be found or does not exist. Exiting.") + return + } +} diff --git a/src/nvm/arch/arch.go b/src/nvm/arch/arch.go new file mode 100644 index 00000000..b7a7f649 --- /dev/null +++ b/src/nvm/arch/arch.go @@ -0,0 +1,64 @@ +package arch + +import ( + //"regexp" + "os" + //"os/exec" + "strings" + //"fmt" + "encoding/hex" +) + +func SearchBytesInFile( path string, match string, limit int) bool { + // Transform to byte array the string + toMatch, err := hex.DecodeString(match); + if (err != nil) { + return false; + } + + // Opening the file and checking if there is an arror + file, err := os.Open(path) + if err != nil { + return false; + } + + // Allocate 1 byte array to perform the match + bit := make([]byte, 1); + j := 0 + for i := 0; i < limit; i++ { + file.Read(bit); + + if bit[0] != toMatch[j] { + j = 0; + } + if bit[0] == toMatch[j] { + j++; + if (j >= len(toMatch)) { + return true; + } + } + } + return false; +} + +func Bit(path string) string { + is64 := SearchBytesInFile(path, "504500006486", 400); + is32 := SearchBytesInFile(path, "504500004C", 400); + if is64 { + return "64"; + } else if is32 { + return "32"; + } + return "?"; +} + +func Validate(str string) (string){ + if str == "" { + str = os.Getenv("PROCESSOR_ARCHITECTURE") + } + if strings.ContainsAny("64",str) { + return "64" + } else { + return "32" + } +} diff --git a/src/nvm/file/file.go b/src/nvm/file/file.go new file mode 100644 index 00000000..f0e80b7d --- /dev/null +++ b/src/nvm/file/file.go @@ -0,0 +1,75 @@ +package file + +import( + "archive/zip" + "bufio" + "log" + "io" + "os" + "path/filepath" + "strings" +) + +// Function courtesy http://stackoverflow.com/users/1129149/swtdrgn +func Unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + fpath := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, f.Mode()) + } else { + var fdir string + if lastIndex := strings.LastIndex(fpath,string(os.PathSeparator)); lastIndex > -1 { + fdir = fpath[:lastIndex] + } + + err = os.MkdirAll(fdir, f.Mode()) + if err != nil { + log.Fatal(err) + return err + } + f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, rc) + if err != nil { + return err + } + } + } + return nil +} + +func ReadLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, scanner.Err() +} + +func Exists(filename string) bool { + _, err := os.Stat(filename); + return err == nil +} diff --git a/src/nvm/node/node.go b/src/nvm/node/node.go new file mode 100644 index 00000000..d7268b27 --- /dev/null +++ b/src/nvm/node/node.go @@ -0,0 +1,205 @@ +package node + +import( + "os/exec" + "strings" + "regexp" + "io/ioutil" + "encoding/json" + "../arch" + "../file" + "../web" + "../semver" +) + +/** + * Returns version, architecture + */ +func GetCurrentVersion() (string, string) { + cmd := exec.Command("node","-v") + str, err := cmd.Output() + if err == nil { + v := strings.Trim(regexp.MustCompile("-.*$").ReplaceAllString(regexp.MustCompile("v").ReplaceAllString(strings.Trim(string(str)," \n\r"),""),"")," \n\r") + cmd := exec.Command("node","-p","console.log(process.execPath)") + str, _ := cmd.Output() + file := strings.Trim(regexp.MustCompile("undefined").ReplaceAllString(string(str),"")," \n\r") + bit := arch.Bit(file) + if (bit == "?"){ + cmd := exec.Command("node", "-e", "console.log(process.arch)" ) + str, err := cmd.Output() + if (err == nil) { + if (string(str) == "x64") { + bit = "64" + } else { + bit = "32" + } + } else { + return v, "Unknown" + } + } + return v, bit + } + return "Unknown","" +} + +func IsVersionInstalled(root string, version string, cpu string) bool { + e32 := file.Exists(root+"\\v"+version+"\\node32.exe") + e64 := file.Exists(root+"\\v"+version+"\\node64.exe") + used := file.Exists(root+"\\v"+version+"\\node.exe") + if cpu == "all" { + return ((e32 || e64) && used) || e32 && e64 + } + if file.Exists(root+"\\v"+version+"\\node"+cpu+".exe") { + return true + } + if ((e32||e64) && used) || (e32 && e64) { + return true + } + if !e32 && !e64 && used && arch.Validate(cpu) == arch.Bit(root+"\\v"+version+"\\node.exe") { + return true + } + if cpu == "32" { + return e32 + } + if cpu == "64" { + return e64 + } + return false +} + +func IsVersionAvailable(v string) bool { + // Check the service to make sure the version is available + avail, _, _, _, _, _ := GetAvailable() + + for _, b := range avail { + if b == v { + return true + } + } + return false +} + +func GetInstalled(root string) []string { + list := make([]string,0) + files, _ := ioutil.ReadDir(root) + for i := len(files) - 1; i >= 0; i-- { + if files[i].IsDir() { + isnode, _ := regexp.MatchString("v",files[i].Name()) + if isnode { + list = append(list,files[i].Name()) + } + } + } + return list +} + +// Sorting +type BySemanticVersion []string +func (s BySemanticVersion) Len() int { + return len(s) +} +func (s BySemanticVersion) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s BySemanticVersion) Less(i, j int) bool { + v1, _ := semver.New(s[i]) + v2, _ := semver.New(s[j]) + return v1.GTE(v2) +} + +// Identifies a version as "LTS" +func isLTS(element map[string]interface{}) bool { + switch datatype := element["lts"].(type) { + case bool: + return datatype + case string: + return true + } + return false +} + +// Identifies a version as "current" +func isCurrent(element map[string]interface{}) bool { + if isLTS(element) { + return false + } + + version, _ := semver.New(element["version"].(string)[1:]) + benchmark, _ := semver.New("1.0.0") + + if version.LT(benchmark) { + return false + } + + return version.Major%2 == 0 +} + +// Identifies a stable old version. +func isStable(element map[string]interface{}) bool { + if isCurrent(element) { + return false + } + + version, _ := semver.New(element["version"].(string)[1:]) + + if (version.Major != 0) { + return false + } + + return version.Minor%2 == 0 +} + +// Identifies an unstable old version. +func isUnstable(element map[string]interface{}) bool { + if isStable(element) { + return false + } + + version, _ := semver.New(element["version"].(string)[1:]) + + if (version.Major != 0) { + return false + } + + return version.Minor%2 != 0 +} + +// Retrieve the remotely available versions +func GetAvailable() ([]string, []string, []string, []string, []string, map[string]string) { + all := make([]string,0) + lts := make([]string,0) + current := make([]string,0) + stable := make([]string,0) + unstable := make([]string,0) + npm := make(map[string]string) + url := web.GetFullNodeUrl("index.json") + + // Check the service to make sure the version is available + text := web.GetRemoteTextFile(url) + + // Parse + var data = make([]map[string]interface{}, 0) + json.Unmarshal([]byte(text), &data); + + for _, element := range data { + + var version = element["version"].(string)[1:] + all = append(all, version) + + if val, ok := element["npm"].(string); ok { + npm[version] = val + } + + if isLTS(element) { + lts = append(lts, version) + } else if isCurrent(element) { + current = append(current, version) + } else if isStable(element) { + stable = append(stable, version) + } else if isUnstable(element) { + unstable = append(unstable, version) + } + } + + return all, lts, current, stable, unstable, npm +} diff --git a/src/nvm/semver/semver.go b/src/nvm/semver/semver.go new file mode 100644 index 00000000..a726a7c6 --- /dev/null +++ b/src/nvm/semver/semver.go @@ -0,0 +1,379 @@ +/** + * Used under the MIT License. + * Semver courtesy Benedikt Lang (https://github.com/blang) + */ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers + dot = "." + hyphen = "-" + plus = "+" +) + +// Latest fully supported spec version +var SPEC_VERSION = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []*PRVersion + Build []string //No Precendence +} + +// Version to string +func (v *Version) String() string { + versionArray := []string{ + strconv.FormatUint(v.Major, 10), + dot, + strconv.FormatUint(v.Minor, 10), + dot, + strconv.FormatUint(v.Patch, 10), + } + if len(v.Pre) > 0 { + versionArray = append(versionArray, hyphen) + for i, pre := range v.Pre { + if i > 0 { + versionArray = append(versionArray, dot) + } + versionArray = append(versionArray, pre.String()) + } + } + if len(v.Build) > 0 { + versionArray = append(versionArray, plus, strings.Join(v.Build, dot)) + } + return strings.Join(versionArray, "") +} + +// Checks if v is greater than o. +func (v *Version) GT(o *Version) bool { + return (v.Compare(o) == 1) +} + +// Checks if v is greater than or equal to o. +func (v *Version) GTE(o *Version) bool { + return (v.Compare(o) >= 0) +} + +// Checks if v is less than o. +func (v *Version) LT(o *Version) bool { + return (v.Compare(o) == -1) +} + +// Checks if v is less than or equal to o. +func (v *Version) LTE(o *Version) bool { + return (v.Compare(o) <= 0) +} + +// Compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v *Version) Compare(o *Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } else { + return -1 + } + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } else { + return -1 + } + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } else { + return -1 + } + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } else { + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + + } +} + +// Validates v and returns error in case +func (v *Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + if len(v.Pre) > 0 { + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + } + + if len(v.Build) > 0 { + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + } + + return nil +} + +// Alias for Parse, parses version string and returns a validated Version or error +func New(s string) (*Version, error) { + return Parse(s) +} + +// Parses version string and returns a validated Version or error +func Parse(s string) (*Version, error) { + if len(s) == 0 { + return nil, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return nil, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return nil, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return nil, err + } + + preIndex := strings.Index(parts[2], "-") + buildIndex := strings.Index(parts[2], "+") + + // Determine last index of patch version (first of pre or build versions) + var subVersionIndex int + if preIndex != -1 && buildIndex == -1 { + subVersionIndex = preIndex + } else if preIndex == -1 && buildIndex != -1 { + subVersionIndex = buildIndex + } else if preIndex == -1 && buildIndex == -1 { + subVersionIndex = len(parts[2]) + } else { + // if there is no actual prversion but a hyphen inside the build meta data + if buildIndex < preIndex { + subVersionIndex = buildIndex + preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions + } else { + subVersionIndex = preIndex + } + } + + if !containsOnly(parts[2][:subVersionIndex], numbers) { + return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) + } + if hasLeadingZeroes(parts[2][:subVersionIndex]) { + return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex]) + } + patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64) + if err != nil { + return nil, err + } + v := &Version{} + v.Major = major + v.Minor = minor + v.Patch = patch + + // There are PreRelease versions + if preIndex != -1 { + var preRels string + if buildIndex != -1 { + preRels = parts[2][subVersionIndex+1 : buildIndex] + } else { + preRels = parts[2][subVersionIndex+1:] + } + prparts := strings.Split(preRels, ".") + for _, prstr := range prparts { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return nil, err + } + v.Pre = append(v.Pre, parsedPR) + } + } + + // There is build meta data + if buildIndex != -1 { + buildStr := parts[2][buildIndex+1:] + buildParts := strings.Split(buildStr, ".") + for _, str := range buildParts { + if len(str) == 0 { + return nil, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + } + + return v, nil +} + +// PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// Creates a new valid prerelease version +func NewPRVersion(s string) (*PRVersion, error) { + if len(s) == 0 { + return nil, errors.New("Prerelease is empty") + } + v := &PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return nil, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// Is pre release version numeric? +func (v *PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compares PreRelease Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v *PRVersion) Compare(o *PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v *PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// Creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} diff --git a/src/nvm/web/web.go b/src/nvm/web/web.go new file mode 100644 index 00000000..fb2f49b3 --- /dev/null +++ b/src/nvm/web/web.go @@ -0,0 +1,197 @@ +package web + +import( + "fmt" + "net/http" + "net/url" + "os" + "io" + "io/ioutil" + "strings" + "strconv" + "../arch" + "../file" +) + +var client = &http.Client{} +var nodeBaseAddress = "https://nodejs.org/dist/" +var npmBaseAddress = "https://github.com/npm/npm/archive/" + +func SetProxy(p string){ + if p != "" && p != "none" { + proxyUrl, _ := url.Parse(p) + client = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)}} + } else { + client = &http.Client{} + } +} + +func SetMirrors(node_mirror string, npm_mirror string){ + if node_mirror != "" && node_mirror != "none"{ + nodeBaseAddress = node_mirror; + if strings.ToLower(nodeBaseAddress[0:4]) != "http" { + nodeBaseAddress = "http://"+nodeBaseAddress + } + } + if npm_mirror != "" && npm_mirror != "none"{ + npmBaseAddress = npm_mirror; + if strings.ToLower(npmBaseAddress[0:4]) != "http" { + npmBaseAddress = "http://"+npmBaseAddress + } + } +} + +func GetFullNodeUrl(path string) string{ + return nodeBaseAddress+ path; +} + +func GetFullNpmUrl(path string) string{ + return npmBaseAddress + path; +} + +func Download(url string, target string) bool { + + output, err := os.Create(target) + if err != nil { + fmt.Println("Error while creating", target, "-", err) + } + defer output.Close() + + response, err := client.Get(url) + if err != nil { + fmt.Println("Error while downloading", url, "-", err) + } + defer response.Body.Close() + + _, err = io.Copy(output, response.Body) + if err != nil { + fmt.Println("Error while downloading", url, "-", err) + } + + if response.Status[0:3] != "200" { + fmt.Println("Download failed. Rolling Back.") + err := os.Remove(target) + if err != nil { + fmt.Println("Rollback failed.",err) + } + return false + } + + return true +} + +func GetNodeJS(root string, v string, a string) bool { + + a = arch.Validate(a) + + vpre := "" + vers := strings.Fields(strings.Replace(v,"."," ",-1)) + main, _ := strconv.ParseInt(vers[0],0,0) + + if a == "32" { + if main > 0 { + vpre = "win-x86/" + } else { + vpre = "" + } + } else if a == "64" { + if main > 0 { + vpre = "win-x64/" + } else { + vpre = "x64/" + } + } + + url := getNodeUrl ( v, vpre ); + + if url == "" { + //No url should mean this version/arch isn't available + fmt.Println("Node.js v"+v+" " + a + "bit isn't available right now.") + } else { + fileName := root+"\\v"+v+"\\node"+a+".exe" + + fmt.Printf("Downloading node.js version "+v+" ("+a+"-bit)... ") + + if Download(url,fileName) { + fmt.Printf("Complete\n") + return true + } else { + return false + } + } + return false + +} + +func GetNpm(root string, v string) bool { + //url := "https://github.com/npm/npm/archive/v"+v+".zip" + url := GetFullNpmUrl("v"+v+".zip") + // temp directory to download the .zip file + tempDir := root+"\\temp" + + // if the temp directory doesn't exist, create it + if (!file.Exists(tempDir)) { + fmt.Println("Creating "+tempDir+"\n") + err := os.Mkdir(tempDir, os.ModePerm) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + fileName := tempDir+"\\"+"npm-v"+v+".zip" + + fmt.Printf("Downloading npm version "+v+"... ") + if Download(url,fileName) { + fmt.Printf("Complete\n") + return true + } else { + return false + } +} + +func GetRemoteTextFile(url string) string { + response, httperr := client.Get(url) + if httperr != nil { + fmt.Println("\nCould not retrieve "+url+".\n\n") + fmt.Printf("%s", httperr) + os.Exit(1) + } else { + defer response.Body.Close() + contents, readerr := ioutil.ReadAll(response.Body) + if readerr != nil { + fmt.Printf("%s", readerr) + os.Exit(1) + } + return string(contents) + } + os.Exit(1) + return "" +} + +func IsNode64bitAvailable(v string) bool { + if v == "latest" { + return true + } + + // Anything below version 8 doesn't have a 64 bit version + vers := strings.Fields(strings.Replace(v,"."," ",-1)) + main, _ := strconv.ParseInt(vers[0],0,0) + minor, _ := strconv.ParseInt(vers[1],0,0) + if main == 0 && minor < 8 { + return false + } + + // TODO: fixme. Assume a 64 bit version exists + return true +} + +func getNodeUrl (v string, vpre string) string { + //url := "http://nodejs.org/dist/v"+v+"/" + vpre + "/node.exe" + url := GetFullNodeUrl("v"+v+"/" + vpre + "/node.exe") + // Check online to see if a 64 bit version exists + _, err := client.Head( url ) + if err != nil { + return "" + } + return url; +} diff --git a/sync b/sync deleted file mode 160000 index 649f37cd..00000000 --- a/sync +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 649f37cd3c5e267aae0d1db4802293c95dc77a62