From a3bd402ef1e75fe1289b76387578b7db91a83817 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 10 Jul 2026 10:06:17 +0900 Subject: [PATCH 1/2] fix: Verify installer scripts before Unity Editor installs The Unity Editor's remote install path used to fetch `install.sh` / `install.ps1` from `raw.githubusercontent.com` via a git tag and execute them without verification. Git tags can be moved after they are published, and `irm | iex` on Windows executes the fetched script straight from memory, so a compromised release could ship an unverified installer to any editor that runs the remote install command. Switch both platforms to the same flow that `uloop update` already uses: - Fetch the installer script and its `.sha256` sidecar from the dispatcher release assets (same generation as `uloop update`). - Verify SHA-256 with `sha256sum`/`shasum` on POSIX and `Get-FileHash` on Windows, comparing case-insensitively against the first token of the `.sha256` file (which contains ` `). - On Windows, replace `irm | iex` with a temp-file download followed by `powershell -NoProfile -ExecutionPolicy Bypass -File $script`, wrapped in `try` / `finally` so the temp dir is removed on failure and `$LASTEXITCODE != 0` re-throws. - Chain the POSIX steps with `&&` (the whole command runs under a single `/bin/sh -c` where `set -e` is not applied for us) so a curl or checksum failure aborts before the script runs. The local script path used during package development is unchanged because it never fetches over the network, and `RAW_CONTENT_BASE_URL` in `CliConstants` is removed since it is no longer referenced from C#. --- .../Tests/Editor/NativeCliInstallerTests.cs | 54 ++++++++++----- Packages/src/Editor/Domain/CliConstants.cs | 2 +- .../CLI/NativeCliCommandBuilder.cs | 67 ++++++++++++++++--- 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 0bcb4eef34..2c80bb1c4b 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -22,7 +22,7 @@ public class NativeCliInstallerTests [Test] public void GetInstallCommand_OnMacKeepsDispatcherCurlInstallerAvailable() { - // Verifies that editor installs use the dispatcher installer script, not npm. + // Verifies that editor installs download the dispatcher installer script and its checksum from the release, then execute it, and never fall back to npm. NativeCliInstallCommand command = NativeCliCommandBuilder.BuildRemoteInstallCommand( RuntimePlatform.OSXEditor, TestBetaCliVersion, @@ -31,19 +31,21 @@ public void GetInstallCommand_OnMacKeepsDispatcherCurlInstallerAvailable() Assert.That(command.FileName, Is.EqualTo("/bin/zsh")); Assert.That(command.Arguments, Does.Contain("-l -i -c")); - Assert.That(command.Arguments, Does.Contain($"https://raw.githubusercontent.com/hatayama/unity-cli-loop/{TestBetaReleaseTag}/scripts/install.sh")); + Assert.That(command.Arguments, Does.Contain($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.sh")); + Assert.That(command.Arguments, Does.Contain($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.sh.sha256")); Assert.That(command.Arguments, Does.Contain($"{CliConstants.POSIX_SHELL_EXECUTABLE_PATH} -c")); Assert.That(command.Arguments, Does.Contain("ULOOP_VERSION")); Assert.That(command.Arguments, Does.Contain(TestBetaReleaseTag)); Assert.That(command.Arguments, Does.Not.Contain("ULOOP_REMOVE_LEGACY")); Assert.That(command.ManualCommand, Does.Contain("curl -fsSL")); Assert.That(command.ManualCommand, Does.Not.Contain("npm")); + Assert.That(command.ManualCommand, Does.Not.Contain("raw.githubusercontent.com")); } [Test] public void GetInstallCommand_OnMacDelegatesPosixSnippetThroughSh() { - // Verifies that fish or other login shells only load environment before POSIX script execution. + // Verifies that fish or other login shells only load environment before the POSIX script downloads, verifies, and executes. NativeCliInstallCommand command = NativeCliCommandBuilder.BuildRemoteInstallCommand( RuntimePlatform.OSXEditor, TestBetaCliVersion, @@ -53,14 +55,16 @@ public void GetInstallCommand_OnMacDelegatesPosixSnippetThroughSh() Assert.That(command.FileName, Is.EqualTo("/opt/homebrew/bin/fish")); Assert.That(command.Arguments, Does.Contain("-l -i -c")); Assert.That(command.ManualCommand, Does.StartWith($"{CliConstants.POSIX_SHELL_EXECUTABLE_PATH} -c ")); - Assert.That(command.ManualCommand, Does.Contain("tmp_script=$(mktemp)")); + Assert.That(command.ManualCommand, Does.Contain("tmp_dir=$(mktemp -d)")); Assert.That(command.ManualCommand, Does.Contain("curl -fsSL")); + Assert.That(command.ManualCommand, Does.Contain("sha256sum -c install.sh.sha256")); + Assert.That(command.ManualCommand, Does.Contain("shasum -a 256 -c install.sh.sha256")); } [Test] public void GetInstallCommand_OnMacPropagatesInstallerDownloadFailure() { - // Verifies that editor installs do not report success when curl fails before script execution. + // Verifies that editor installs do not report success when curl or checksum verification fails before script execution. NativeCliInstallCommand command = NativeCliCommandBuilder.BuildRemoteInstallCommand( RuntimePlatform.OSXEditor, TestBetaCliVersion, @@ -76,7 +80,7 @@ public void GetInstallCommand_OnMacPropagatesInstallerDownloadFailure() [Test] public void GetInstallCommand_OnWindowsKeepsDispatcherPowerShellInstallerAvailable() { - // Verifies that editor installs use the dispatcher PowerShell installer script. + // Verifies that editor installs download the dispatcher PowerShell installer script and its checksum from the release, verify SHA-256, and run via -File, not `irm | iex`. NativeCliInstallCommand command = NativeCliCommandBuilder.BuildRemoteInstallCommand( RuntimePlatform.WindowsEditor, TestBetaCliVersion, @@ -84,10 +88,16 @@ public void GetInstallCommand_OnWindowsKeepsDispatcherPowerShellInstallerAvailab "/bin/zsh"); Assert.That(command.FileName, Is.EqualTo("powershell")); - Assert.That(command.Arguments, Does.Contain($"https://raw.githubusercontent.com/hatayama/unity-cli-loop/{TestBetaReleaseTag}/scripts/install.ps1")); - Assert.That(command.Arguments, Does.Contain($"$env:ULOOP_VERSION='{TestBetaReleaseTag}'")); + Assert.That(command.Arguments, Does.Contain($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.ps1")); + Assert.That(command.Arguments, Does.Contain($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.ps1.sha256")); + Assert.That(command.Arguments, Does.Contain($"$env:ULOOP_VERSION = '{TestBetaReleaseTag}'")); Assert.That(command.Arguments, Does.Not.Contain("ULOOP_REMOVE_LEGACY")); - Assert.That(command.ManualCommand, Does.Contain("irm")); + Assert.That(command.ManualCommand, Does.Contain("Invoke-WebRequest")); + Assert.That(command.ManualCommand, Does.Contain("Get-FileHash")); + Assert.That(command.ManualCommand, Does.Contain("-File $script_path")); + Assert.That(command.ManualCommand, Does.Not.Contain("irm")); + Assert.That(command.ManualCommand, Does.Not.Contain("| iex")); + Assert.That(command.ManualCommand, Does.Not.Contain("raw.githubusercontent.com")); Assert.That(command.ManualCommand, Does.Not.Contain("npm")); } @@ -132,7 +142,7 @@ public void GetInstallCommand_WhenVPrefixedVersionUsesDispatcherReleaseTag() Assert.That(command.Arguments, Does.Contain("dispatcher-v3.0.0")); Assert.That( command.Arguments, - Does.Contain("https://raw.githubusercontent.com/hatayama/unity-cli-loop/dispatcher-v3.0.0/scripts/install.sh")); + Does.Contain("https://github.com/hatayama/unity-cli-loop/releases/download/dispatcher-v3.0.0/install.sh")); } [Test] @@ -225,25 +235,37 @@ public void GetInstallCommand_WhenLocalPackageOnWindowsUsesPackageLocalInstaller } [Test] - public void BuildInstallerScriptUrl_WhenBetaVersionUsesReleaseInstallerScript() + public void BuildInstallerScriptUrl_WhenBetaVersionUsesReleaseInstallerAsset() { - // Verifies that beta editor installs use the script shipped with the selected dispatcher release. + // Verifies that beta editor installs point at the release-asset install.sh, matching `uloop update`'s verified download URL. string url = NativeCliCommandBuilder.BuildInstallerScriptUrl( TestBetaReleaseTag, CliConstants.POSIX_INSTALL_SCRIPT_NAME); - Assert.That(url, Is.EqualTo($"https://raw.githubusercontent.com/hatayama/unity-cli-loop/{TestBetaReleaseTag}/scripts/install.sh")); + Assert.That(url, Is.EqualTo($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.sh")); } [Test] - public void BuildInstallerScriptUrl_WhenStableVersionUsesReleaseInstallerScript() + public void BuildInstallerScriptUrl_WhenStableVersionUsesReleaseInstallerAsset() { - // Verifies that stable editor installs use the script shipped with the selected dispatcher release. + // Verifies that stable editor installs point at the release-asset install.ps1, matching `uloop update`'s verified download URL. string url = NativeCliCommandBuilder.BuildInstallerScriptUrl( TestStableReleaseTag, CliConstants.WINDOWS_INSTALL_SCRIPT_NAME); - Assert.That(url, Is.EqualTo($"https://raw.githubusercontent.com/hatayama/unity-cli-loop/{TestStableReleaseTag}/scripts/install.ps1")); + Assert.That(url, Is.EqualTo($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestStableReleaseTag}/install.ps1")); + } + + [Test] + public void BuildInstallerChecksumUrl_AppendsShaSuffixToScriptUrl() + { + // Verifies the checksum URL points at the release-asset .sha256 sidecar next to the installer script. + string scriptUrl = NativeCliCommandBuilder.BuildInstallerScriptUrl( + TestBetaReleaseTag, + CliConstants.POSIX_INSTALL_SCRIPT_NAME); + string checksumUrl = NativeCliCommandBuilder.BuildInstallerChecksumUrl(scriptUrl); + + Assert.That(checksumUrl, Is.EqualTo(scriptUrl + ".sha256")); } [Test] diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index a66485a8c6..a311eb3fcb 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -13,7 +13,7 @@ public static class CliConstants public const string VERSION_FLAG = "--version"; public const string SHORT_VERSION_FLAG = "-v"; public const string JSON_FLAG = "--json"; - public const string RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com/hatayama/unity-cli-loop"; + public const string RELEASE_DOWNLOAD_BASE_URL = "https://github.com/hatayama/unity-cli-loop/releases/download"; public const string SCRIPTS_DIR_NAME = "scripts"; public const string POSIX_INSTALL_SCRIPT_NAME = "install.sh"; public const string WINDOWS_INSTALL_SCRIPT_NAME = "install.ps1"; diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs index 5698b28606..edb7226e0e 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs @@ -44,9 +44,11 @@ internal static NativeCliInstallCommand BuildInstallCommandWithPackagePath( string localScriptPath = ResolvePackageLocalInstallerScriptPath( packageResolvedPath, CliConstants.WINDOWS_INSTALL_SCRIPT_NAME); + string windowsScriptUrl = BuildInstallerScriptUrl(releaseTag, CliConstants.WINDOWS_INSTALL_SCRIPT_NAME); string command = string.IsNullOrEmpty(localScriptPath) ? BuildWindowsRemoteInstallScriptCommand( - BuildInstallerScriptUrl(releaseTag, CliConstants.WINDOWS_INSTALL_SCRIPT_NAME), + windowsScriptUrl, + BuildInstallerChecksumUrl(windowsScriptUrl), releaseTag) : BuildWindowsLocalInstallScriptCommand(localScriptPath, releaseTag); return new NativeCliInstallCommand( @@ -58,9 +60,11 @@ internal static NativeCliInstallCommand BuildInstallCommandWithPackagePath( string posixLocalScriptPath = ResolvePackageLocalInstallerScriptPath( packageResolvedPath, CliConstants.POSIX_INSTALL_SCRIPT_NAME); + string posixScriptUrl = BuildInstallerScriptUrl(releaseTag, CliConstants.POSIX_INSTALL_SCRIPT_NAME); string posixCommand = string.IsNullOrEmpty(posixLocalScriptPath) ? BuildPosixRemoteInstallScriptCommand( - BuildInstallerScriptUrl(releaseTag, CliConstants.POSIX_INSTALL_SCRIPT_NAME), + posixScriptUrl, + BuildInstallerChecksumUrl(posixScriptUrl), releaseTag) : BuildPosixLocalInstallScriptCommand(posixLocalScriptPath, releaseTag); string loginShellCommand = BuildLoginShellPosixInstallScriptCommand(posixCommand); @@ -83,15 +87,25 @@ internal static NativeCliInstallCommand BuildUninstallCommand( $"{QuoteProcessArgument(installPath)} uninstall"); } - private static string BuildPosixRemoteInstallScriptCommand(string scriptUrl, string releaseTag) + private static string BuildPosixRemoteInstallScriptCommand(string scriptUrl, string checksumUrl, string releaseTag) { UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(scriptUrl), "scriptUrl must not be null or empty"); + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(checksumUrl), "checksumUrl must not be null or empty"); UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(releaseTag), "releaseTag must not be null or empty"); - return "tmp_script=$(mktemp) && " - + "trap 'rm -f \"$tmp_script\"' EXIT && " - + $"curl -fsSL {QuotePosixShellValue(scriptUrl)} -o \"$tmp_script\" && " - + $"{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE}={QuotePosixShellValue(releaseTag)} sh \"$tmp_script\""; + // Why: The whole command runs as a single /bin/sh -c string where `set -e` is not applied + // for us, so every step is && chained to fail fast when curl, checksum verification, or + // execution fails. `trap ... EXIT` ensures the temp directory is removed even on failure. + string scriptName = CliConstants.POSIX_INSTALL_SCRIPT_NAME; + return "tmp_dir=$(mktemp -d) && " + + "trap 'rm -rf \"$tmp_dir\"' EXIT && " + + $"curl -fsSL {QuotePosixShellValue(scriptUrl)} -o \"$tmp_dir/{scriptName}\" && " + + $"curl -fsSL {QuotePosixShellValue(checksumUrl)} -o \"$tmp_dir/{scriptName}.sha256\" && " + + "( cd \"$tmp_dir\" && " + + $"if command -v sha256sum >/dev/null 2>&1; then sha256sum -c {scriptName}.sha256; " + + $"elif command -v shasum >/dev/null 2>&1; then shasum -a 256 -c {scriptName}.sha256; " + + $"else echo 'sha256sum or shasum is required to verify {scriptName}' >&2; exit 1; fi ) && " + + $"{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE}={QuotePosixShellValue(releaseTag)} sh \"$tmp_dir/{scriptName}\""; } private static string BuildPosixLocalInstallScriptCommand(string scriptPath, string releaseTag) @@ -108,13 +122,34 @@ internal static string BuildLoginShellPosixInstallScriptCommand(string posixComm return $"{CliConstants.POSIX_SHELL_EXECUTABLE_PATH} -c {QuotePosixShellValue(posixCommand)}"; } - private static string BuildWindowsRemoteInstallScriptCommand(string scriptUrl, string releaseTag) + private static string BuildWindowsRemoteInstallScriptCommand(string scriptUrl, string checksumUrl, string releaseTag) { UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(scriptUrl), "scriptUrl must not be null or empty"); + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(checksumUrl), "checksumUrl must not be null or empty"); UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(releaseTag), "releaseTag must not be null or empty"); - return $"$env:{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE}={QuotePowerShellSingleQuotedValue(releaseTag)}; " - + $"irm {QuotePowerShellSingleQuotedValue(scriptUrl)} | iex"; + // Why: Downloading with Invoke-WebRequest to a file and re-launching PowerShell with -File + // replaces the previous `irm | iex` streaming path so the script is verified before it runs + // and so the child process owns exit-code propagation. The .sha256 file contains + // " ", so the leading whitespace-delimited token is compared as lower + // case against Get-FileHash's upper-case output. + string scriptName = CliConstants.WINDOWS_INSTALL_SCRIPT_NAME; + return "$tmp_dir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())) -Force; " + + $"$script_path = Join-Path $tmp_dir.FullName {QuotePowerShellSingleQuotedValue(scriptName)}; " + + $"$checksum_path = Join-Path $tmp_dir.FullName {QuotePowerShellSingleQuotedValue(scriptName + ".sha256")}; " + + "try { " + + "$ProgressPreference = 'SilentlyContinue'; " + + $"Invoke-WebRequest -UseBasicParsing -Uri {QuotePowerShellSingleQuotedValue(scriptUrl)} -OutFile $script_path; " + + $"Invoke-WebRequest -UseBasicParsing -Uri {QuotePowerShellSingleQuotedValue(checksumUrl)} -OutFile $checksum_path; " + + "$expected_hash = ((Get-Content -Raw -Encoding UTF8 $checksum_path) -split '\\s+')[0].ToLowerInvariant(); " + + "$actual_hash = (Get-FileHash -Algorithm SHA256 -Path $script_path).Hash.ToLowerInvariant(); " + + $"if ($actual_hash -ne $expected_hash) {{ throw ('Checksum mismatch for {scriptName}: expected=' + $expected_hash + ' actual=' + $actual_hash) }}; " + + $"$env:{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE} = {QuotePowerShellSingleQuotedValue(releaseTag)}; " + + "& powershell -NoProfile -ExecutionPolicy Bypass -File $script_path; " + + $"if ($LASTEXITCODE -ne 0) {{ throw ('{scriptName} exited with code ' + $LASTEXITCODE) }} " + + "} finally { " + + "Remove-Item -Recurse -Force -LiteralPath $tmp_dir.FullName -ErrorAction SilentlyContinue " + + "}"; } private static string BuildWindowsLocalInstallScriptCommand(string scriptPath, string releaseTag) @@ -196,7 +231,17 @@ internal static string BuildInstallerScriptUrl(string releaseTag, string assetNa UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(releaseTag), "releaseTag must not be null or empty"); UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(assetName), "assetName must not be null or empty"); - return $"{CliConstants.RAW_CONTENT_BASE_URL}/{releaseTag}/{CliConstants.SCRIPTS_DIR_NAME}/{assetName}"; + // Why: Installer scripts are shipped as dispatcher release assets alongside their .sha256 + // sidecars, so the release download URL is used instead of raw content refs to keep the + // Unity install path in sync with `uloop update`'s verified installer download. + return $"{CliConstants.RELEASE_DOWNLOAD_BASE_URL}/{releaseTag}/{assetName}"; + } + + internal static string BuildInstallerChecksumUrl(string scriptUrl) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(scriptUrl), "scriptUrl must not be null or empty"); + + return scriptUrl + ".sha256"; } } } From a1d0a8c350f1f0179a797241f34ce69e243c6d05 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 10 Jul 2026 10:20:48 +0900 Subject: [PATCH 2/2] fix: Stop the Windows installer flow on the first cmdlet error Set `$ErrorActionPreference = 'Stop'` inside the try block so that any non-terminating cmdlet error (missing checksum file, unexpected Get-FileHash result, etc.) throws immediately, instead of relying on a downstream null dereference to eventually raise an exception. The existing fail-close paths still work as before, but the intent is now explicit and no longer coupled to incidental error semantics. --- Assets/Tests/Editor/NativeCliInstallerTests.cs | 1 + .../Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 2c80bb1c4b..279f4a0a76 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -92,6 +92,7 @@ public void GetInstallCommand_OnWindowsKeepsDispatcherPowerShellInstallerAvailab Assert.That(command.Arguments, Does.Contain($"https://github.com/hatayama/unity-cli-loop/releases/download/{TestBetaReleaseTag}/install.ps1.sha256")); Assert.That(command.Arguments, Does.Contain($"$env:ULOOP_VERSION = '{TestBetaReleaseTag}'")); Assert.That(command.Arguments, Does.Not.Contain("ULOOP_REMOVE_LEGACY")); + Assert.That(command.ManualCommand, Does.Contain("$ErrorActionPreference = 'Stop'")); Assert.That(command.ManualCommand, Does.Contain("Invoke-WebRequest")); Assert.That(command.ManualCommand, Does.Contain("Get-FileHash")); Assert.That(command.ManualCommand, Does.Contain("-File $script_path")); diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs index edb7226e0e..45924f6194 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs @@ -130,14 +130,17 @@ private static string BuildWindowsRemoteInstallScriptCommand(string scriptUrl, s // Why: Downloading with Invoke-WebRequest to a file and re-launching PowerShell with -File // replaces the previous `irm | iex` streaming path so the script is verified before it runs - // and so the child process owns exit-code propagation. The .sha256 file contains - // " ", so the leading whitespace-delimited token is compared as lower - // case against Get-FileHash's upper-case output. + // and so the child process owns exit-code propagation. `$ErrorActionPreference = 'Stop'` + // upgrades cmdlet non-terminating errors (e.g. missing checksum file) into throws so the + // fail-close path does not depend on incidental null dereferences downstream. The .sha256 + // file contains " ", so the leading whitespace-delimited token is + // compared as lower case against Get-FileHash's upper-case output. string scriptName = CliConstants.WINDOWS_INSTALL_SCRIPT_NAME; return "$tmp_dir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())) -Force; " + $"$script_path = Join-Path $tmp_dir.FullName {QuotePowerShellSingleQuotedValue(scriptName)}; " + $"$checksum_path = Join-Path $tmp_dir.FullName {QuotePowerShellSingleQuotedValue(scriptName + ".sha256")}; " + "try { " + + "$ErrorActionPreference = 'Stop'; " + "$ProgressPreference = 'SilentlyContinue'; " + $"Invoke-WebRequest -UseBasicParsing -Uri {QuotePowerShellSingleQuotedValue(scriptUrl)} -OutFile $script_path; " + $"Invoke-WebRequest -UseBasicParsing -Uri {QuotePowerShellSingleQuotedValue(checksumUrl)} -OutFile $checksum_path; "