Unify release selection across clients
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
@@ -88,6 +88,9 @@ jobs:
|
|||||||
$errors | ForEach-Object { Write-Error $_ }
|
$errors | ForEach-Object { Write-Error $_ }
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
- name: Windows installer runtime test
|
||||||
|
shell: pwsh
|
||||||
|
run: scripts/test-install-windows.ps1
|
||||||
- name: cmd installer smoke check
|
- name: cmd installer smoke check
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+36
-132
@@ -11,14 +11,17 @@ param(
|
|||||||
[string]$BinaryBase = $env:DOSH_BINARY_BASE,
|
[string]$BinaryBase = $env:DOSH_BINARY_BASE,
|
||||||
[string]$BinaryName = $env:DOSH_BINARY_NAME,
|
[string]$BinaryName = $env:DOSH_BINARY_NAME,
|
||||||
[string]$BinaryVersion = $(if ($env:DOSH_BINARY_VERSION) { $env:DOSH_BINARY_VERSION } else { "latest" }),
|
[string]$BinaryVersion = $(if ($env:DOSH_BINARY_VERSION) { $env:DOSH_BINARY_VERSION } else { "latest" }),
|
||||||
|
[switch]$BinaryExact = $($env:DOSH_BINARY_EXACT -and $env:DOSH_BINARY_EXACT -ne "0"),
|
||||||
[string]$UpdateCache = $(if ($env:DOSH_UPDATE_CACHE) { $env:DOSH_UPDATE_CACHE } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "dosh\source" } else { Join-Path $HOME ".cache\dosh\source" }),
|
[string]$UpdateCache = $(if ($env:DOSH_UPDATE_CACHE) { $env:DOSH_UPDATE_CACHE } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "dosh\source" } else { Join-Path $HOME ".cache\dosh\source" }),
|
||||||
[switch]$BinaryRequired = $($env:DOSH_BINARY_REQUIRED -and $env:DOSH_BINARY_REQUIRED -ne "0"),
|
[switch]$BinaryRequired = $($env:DOSH_BINARY_REQUIRED -and $env:DOSH_BINARY_REQUIRED -ne "0"),
|
||||||
|
[switch]$FromCurrent,
|
||||||
[switch]$ForceConfig
|
[switch]$ForceConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$ProgressPreference = "SilentlyContinue"
|
$ProgressPreference = "SilentlyContinue"
|
||||||
$Quiet = $env:DOSH_UPDATE_QUIET -and $env:DOSH_UPDATE_QUIET -ne "0"
|
$Quiet = $env:DOSH_UPDATE_QUIET -and $env:DOSH_UPDATE_QUIET -ne "0"
|
||||||
|
$script:ResolvedSourceRef = $null
|
||||||
|
|
||||||
function Write-Info($Message) {
|
function Write-Info($Message) {
|
||||||
if (-not $Quiet) {
|
if (-not $Quiet) {
|
||||||
@@ -121,96 +124,18 @@ function Release-DownloadUrl {
|
|||||||
if (-not $web) {
|
if (-not $web) {
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
if ($BinaryVersion -eq "latest") {
|
$releaseRef = Source-ReleaseRef
|
||||||
return "$web/releases/latest/download/$name"
|
if (-not $releaseRef) {
|
||||||
}
|
|
||||||
"$web/releases/download/$BinaryVersion/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Release-LatestTagDownloadUrl {
|
|
||||||
if ($BinaryUrl -or $BinaryBase -or $BinaryVersion -ne "latest") {
|
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
$web = Repo-WebBase $Repo
|
"$web/releases/download/$releaseRef/$name"
|
||||||
if (-not $web) {
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$web/releases/latest" -MaximumRedirection 5
|
|
||||||
$effective = $null
|
|
||||||
if ($response.BaseResponse.ResponseUri) {
|
|
||||||
$effective = $response.BaseResponse.ResponseUri.AbsoluteUri
|
|
||||||
} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {
|
|
||||||
$effective = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
|
|
||||||
}
|
|
||||||
$prefix = "$web/releases/tag/"
|
|
||||||
if ($effective -and $effective.StartsWith($prefix)) {
|
|
||||||
$tag = $effective.Substring($prefix.Length)
|
|
||||||
if ($tag) {
|
|
||||||
return "$web/releases/download/$tag/$(Release-ArtifactName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Version-Core($Value) {
|
|
||||||
$base = $Value.TrimStart("v").Split("+")[0]
|
|
||||||
$base.Split("-")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function Version-Prerelease($Value) {
|
|
||||||
$base = $Value.TrimStart("v").Split("+")[0]
|
|
||||||
$dash = $base.IndexOf("-")
|
|
||||||
if ($dash -lt 0) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
$base.Substring($dash + 1).ToLowerInvariant()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Version-Parts($Value) {
|
|
||||||
[regex]::Matches((Version-Core $Value), "\d+") | ForEach-Object { [int64]$_.Value }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Compare-Prerelease($Left, $Right) {
|
|
||||||
if (-not $Left -and -not $Right) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (-not $Left) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if (-not $Right) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if ($Left -eq $Right) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if ($Left -match "(?i)^rc(\d+)$" -and $Right -match "(?i)^rc(\d+)$") {
|
|
||||||
$leftRc = [int64]([regex]::Match($Left, "(?i)^rc(\d+)$").Groups[1].Value)
|
|
||||||
$rightRc = [int64]([regex]::Match($Right, "(?i)^rc(\d+)$").Groups[1].Value)
|
|
||||||
return $leftRc.CompareTo($rightRc)
|
|
||||||
}
|
|
||||||
return [string]::CompareOrdinal($Left, $Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Compare-DoshVersion($Left, $Right) {
|
|
||||||
$leftParts = @(Version-Parts $Left)
|
|
||||||
$rightParts = @(Version-Parts $Right)
|
|
||||||
$width = [Math]::Max($leftParts.Count, $rightParts.Count)
|
|
||||||
for ($i = 0; $i -lt $width; $i++) {
|
|
||||||
$l = if ($i -lt $leftParts.Count) { $leftParts[$i] } else { 0 }
|
|
||||||
$r = if ($i -lt $rightParts.Count) { $rightParts[$i] } else { 0 }
|
|
||||||
if ($l -lt $r) { return -1 }
|
|
||||||
if ($l -gt $r) { return 1 }
|
|
||||||
}
|
|
||||||
return Compare-Prerelease (Version-Prerelease $Left) (Version-Prerelease $Right)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Current-SourceVersion {
|
function Current-SourceVersion {
|
||||||
if (Test-Path "Cargo.toml") {
|
if ($FromCurrent) {
|
||||||
|
if (-not (Test-Path "Cargo.toml")) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
$raw = Get-Content "Cargo.toml" -Raw
|
$raw = Get-Content "Cargo.toml" -Raw
|
||||||
} else {
|
} else {
|
||||||
$web = Repo-WebBase $Repo
|
$web = Repo-WebBase $Repo
|
||||||
@@ -230,47 +155,23 @@ function Current-SourceVersion {
|
|||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
function Latest-ReleaseTag {
|
function Source-ReleaseRef {
|
||||||
$web = Repo-WebBase $Repo
|
if ($BinaryUrl -or $BinaryBase) {
|
||||||
if (-not $web) {
|
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
try {
|
if ($BinaryVersion -ne "latest" -and $BinaryExact) {
|
||||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$web/releases/latest" -MaximumRedirection 5
|
return $BinaryVersion
|
||||||
$effective = $null
|
|
||||||
if ($response.BaseResponse.ResponseUri) {
|
|
||||||
$effective = $response.BaseResponse.ResponseUri.AbsoluteUri
|
|
||||||
} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {
|
|
||||||
$effective = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
|
|
||||||
}
|
|
||||||
$prefix = "$web/releases/tag/"
|
|
||||||
if ($effective -and $effective.StartsWith($prefix)) {
|
|
||||||
return $effective.Substring($prefix.Length)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch {
|
$current = Current-SourceVersion
|
||||||
return $null
|
if ($current) {
|
||||||
|
return "v$($current.TrimStart('v'))"
|
||||||
|
}
|
||||||
|
if ($BinaryVersion -ne "latest") {
|
||||||
|
return $BinaryVersion
|
||||||
}
|
}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
function Latest-ReleaseIsStale {
|
|
||||||
if ($BinaryUrl -or $BinaryBase -or $BinaryVersion -ne "latest") {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
$latest = Latest-ReleaseTag
|
|
||||||
$current = Current-SourceVersion
|
|
||||||
if (-not $latest -or -not $current) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
$latestVersion = $latest.TrimStart("v")
|
|
||||||
if ((Compare-DoshVersion $latestVersion $current) -lt 0) {
|
|
||||||
Write-Warning "latest release $latestVersion is older than source $current; skipping stale prebuilt"
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Verify-ArchiveChecksum($Url, $Archive) {
|
function Verify-ArchiveChecksum($Url, $Archive) {
|
||||||
$checksumPath = "$Archive.sha256"
|
$checksumPath = "$Archive.sha256"
|
||||||
try {
|
try {
|
||||||
@@ -475,16 +376,11 @@ if ($env:DOSH_INSTALL_BINDIR_FILE) {
|
|||||||
Apply-PendingBinaryReplacements $bindir
|
Apply-PendingBinaryReplacements $bindir
|
||||||
|
|
||||||
function Install-Prebuilt {
|
function Install-Prebuilt {
|
||||||
if (Latest-ReleaseIsStale) {
|
$url = Release-DownloadUrl
|
||||||
return $false
|
|
||||||
}
|
|
||||||
$url = Release-LatestTagDownloadUrl
|
|
||||||
if (-not $url) {
|
|
||||||
$url = Release-DownloadUrl
|
|
||||||
}
|
|
||||||
if (-not $url) {
|
if (-not $url) {
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
$script:ResolvedSourceRef = Source-ReleaseRef
|
||||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-bin-" + [guid]::NewGuid())
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-bin-" + [guid]::NewGuid())
|
||||||
$zip = Join-Path $tmp (Release-ArtifactName)
|
$zip = Join-Path $tmp (Release-ArtifactName)
|
||||||
$extract = Join-Path $tmp "extract"
|
$extract = Join-Path $tmp "extract"
|
||||||
@@ -520,7 +416,10 @@ function Install-Prebuilt {
|
|||||||
|
|
||||||
function Install-FromSource {
|
function Install-FromSource {
|
||||||
Ensure-Cargo
|
Ensure-Cargo
|
||||||
if (Test-Path "Cargo.toml") {
|
if ($FromCurrent) {
|
||||||
|
if (-not (Test-Path "Cargo.toml")) {
|
||||||
|
throw "-FromCurrent requires a Dosh checkout with Cargo.toml"
|
||||||
|
}
|
||||||
$src = (Get-Location).Path
|
$src = (Get-Location).Path
|
||||||
} else {
|
} else {
|
||||||
if (-not $Repo) {
|
if (-not $Repo) {
|
||||||
@@ -530,21 +429,26 @@ function Install-FromSource {
|
|||||||
$sourceCache = Assert-SafeUpdateCache $UpdateCache
|
$sourceCache = Assert-SafeUpdateCache $UpdateCache
|
||||||
$parent = Split-Path -Parent $sourceCache
|
$parent = Split-Path -Parent $sourceCache
|
||||||
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
||||||
|
$sourceRef = if ($script:ResolvedSourceRef) { $script:ResolvedSourceRef } else { "main" }
|
||||||
if (Test-Path (Join-Path $sourceCache ".git")) {
|
if (Test-Path (Join-Path $sourceCache ".git")) {
|
||||||
git -C $sourceCache remote set-url origin $Repo
|
git -C $sourceCache remote set-url origin $Repo
|
||||||
$fetchArgs = @("-C", $sourceCache, "fetch", "--depth", "1", "origin", "main")
|
$fetchArgs = @("-C", $sourceCache, "fetch", "--depth", "1", "origin", $sourceRef)
|
||||||
if ($Quiet) {
|
if ($Quiet) {
|
||||||
$fetchArgs = @("-C", $sourceCache, "fetch", "-q", "--depth", "1", "origin", "main")
|
$fetchArgs = @("-C", $sourceCache, "fetch", "-q", "--depth", "1", "origin", $sourceRef)
|
||||||
}
|
}
|
||||||
git @fetchArgs
|
git @fetchArgs
|
||||||
git -C $sourceCache checkout -q -B main FETCH_HEAD
|
if ($sourceRef -eq "main") {
|
||||||
|
git -C $sourceCache checkout -q -B main FETCH_HEAD
|
||||||
|
} else {
|
||||||
|
git -C $sourceCache checkout -q --detach FETCH_HEAD
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Test-Path $sourceCache) {
|
if (Test-Path $sourceCache) {
|
||||||
Remove-Item -Recurse -Force $sourceCache
|
Remove-Item -Recurse -Force $sourceCache
|
||||||
}
|
}
|
||||||
$cloneArgs = @("clone", "--depth", "1", "--branch", "main", $Repo, $sourceCache)
|
$cloneArgs = @("clone", "--depth", "1", "--branch", $sourceRef, $Repo, $sourceCache)
|
||||||
if ($Quiet) {
|
if ($Quiet) {
|
||||||
$cloneArgs = @("clone", "-q", "--depth", "1", "--branch", "main", $Repo, $sourceCache)
|
$cloneArgs = @("clone", "-q", "--depth", "1", "--branch", $sourceRef, $Repo, $sourceCache)
|
||||||
}
|
}
|
||||||
git @cloneArgs | Out-Null
|
git @cloneArgs | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-122
@@ -17,6 +17,7 @@ binary_url="${DOSH_BINARY_URL:-}"
|
|||||||
binary_base="${DOSH_BINARY_BASE:-}"
|
binary_base="${DOSH_BINARY_BASE:-}"
|
||||||
binary_name="${DOSH_BINARY_NAME:-}"
|
binary_name="${DOSH_BINARY_NAME:-}"
|
||||||
binary_version="${DOSH_BINARY_VERSION:-latest}"
|
binary_version="${DOSH_BINARY_VERSION:-latest}"
|
||||||
|
binary_exact="${DOSH_BINARY_EXACT:-0}"
|
||||||
binary_required="${DOSH_BINARY_REQUIRED:-0}"
|
binary_required="${DOSH_BINARY_REQUIRED:-0}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -47,7 +48,9 @@ Environment alternatives:
|
|||||||
DOSH_BINARY_NAME NAME
|
DOSH_BINARY_NAME NAME
|
||||||
Release tarball name; defaults to dosh-OS-ARCH.tar.gz
|
Release tarball name; defaults to dosh-OS-ARCH.tar.gz
|
||||||
DOSH_BINARY_VERSION TAG
|
DOSH_BINARY_VERSION TAG
|
||||||
Release tag when deriving DOSH_BINARY_BASE; default latest
|
Requested release tag; repository version wins unless exact
|
||||||
|
DOSH_BINARY_EXACT=1
|
||||||
|
Require DOSH_BINARY_VERSION exactly, including downgrades
|
||||||
DOSH_BINARY_REQUIRED=1
|
DOSH_BINARY_REQUIRED=1
|
||||||
Fail instead of falling back to source when binary install fails
|
Fail instead of falling back to source when binary install fails
|
||||||
EOF
|
EOF
|
||||||
@@ -173,6 +176,7 @@ config_dir="$HOME/.config/dosh"
|
|||||||
data_dir="$HOME/.local/share/dosh"
|
data_dir="$HOME/.local/share/dosh"
|
||||||
systemd_user_dir="$HOME/.config/systemd/user"
|
systemd_user_dir="$HOME/.config/systemd/user"
|
||||||
src_dir=""
|
src_dir=""
|
||||||
|
resolved_source_ref=""
|
||||||
|
|
||||||
mkdir -p "$bindir" "$config_dir" "$data_dir"
|
mkdir -p "$bindir" "$config_dir" "$data_dir"
|
||||||
|
|
||||||
@@ -228,97 +232,13 @@ release_download_url() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
web_base="$(repo_web_base "$repo")" || return 1
|
web_base="$(repo_web_base "$repo")" || return 1
|
||||||
if [ "$binary_version" = "latest" ]; then
|
release_ref="$(release_source_ref)" || return 1
|
||||||
printf '%s/releases/latest/download/%s\n' "$web_base" "$(release_artifact_name)"
|
printf '%s/releases/download/%s/%s\n' "$web_base" "$release_ref" "$(release_artifact_name)"
|
||||||
else
|
|
||||||
printf '%s/releases/download/%s/%s\n' "$web_base" "$binary_version" "$(release_artifact_name)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
release_latest_tag_download_url() {
|
|
||||||
if [ -n "$binary_url" ] || [ -n "$binary_base" ] || [ "$binary_version" != "latest" ] || [ -z "$repo" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
web_base="$(repo_web_base "$repo")" || return 1
|
|
||||||
latest_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' "$web_base/releases/latest" 2>/dev/null || true)"
|
|
||||||
case "$latest_url" in
|
|
||||||
"$web_base"/releases/tag/*)
|
|
||||||
tag="${latest_url##"$web_base"/releases/tag/}"
|
|
||||||
[ -n "$tag" ] || return 1
|
|
||||||
printf '%s/releases/download/%s/%s\n' "$web_base" "$tag" "$(release_artifact_name)"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
version_core() {
|
|
||||||
value="${1#v}"
|
|
||||||
value="${value%%+*}"
|
|
||||||
printf '%s\n' "${value%%-*}"
|
|
||||||
}
|
|
||||||
|
|
||||||
version_prerelease() {
|
|
||||||
value="${1#v}"
|
|
||||||
value="${value%%+*}"
|
|
||||||
case "$value" in
|
|
||||||
*-*) printf '%s\n' "${value#*-}" ;;
|
|
||||||
*) printf '\n' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
version_core_part() {
|
|
||||||
version="$1"
|
|
||||||
index="$2"
|
|
||||||
version_core "$version" \
|
|
||||||
| sed 's/[^0-9][^0-9]*/ /g' \
|
|
||||||
| awk -v index="$index" '{ value=$index; if (value == "") value=0; print value }'
|
|
||||||
}
|
|
||||||
|
|
||||||
prerelease_less_than() {
|
|
||||||
left="$1"
|
|
||||||
right="$2"
|
|
||||||
if [ -z "$left" ] && [ -z "$right" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ -z "$left" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ -z "$right" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ "$left" = "$right" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
left_rc="$(printf '%s\n' "$left" | sed -n 's/^[Rr][Cc]\([0-9][0-9]*\)$/\1/p')"
|
|
||||||
right_rc="$(printf '%s\n' "$right" | sed -n 's/^[Rr][Cc]\([0-9][0-9]*\)$/\1/p')"
|
|
||||||
if [ -n "$left_rc" ] && [ -n "$right_rc" ]; then
|
|
||||||
[ "$left_rc" -lt "$right_rc" ]
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
first="$(printf '%s\n%s\n' "$left" "$right" | LC_ALL=C sort | sed -n '1p')"
|
|
||||||
[ "$first" = "$left" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
version_less_than() {
|
|
||||||
left="$1"
|
|
||||||
right="$2"
|
|
||||||
for index in 1 2 3 4 5 6 7 8; do
|
|
||||||
lpart="$(version_core_part "$left" "$index")"
|
|
||||||
rpart="$(version_core_part "$right" "$index")"
|
|
||||||
if [ "$lpart" -lt "$rpart" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ "$lpart" -gt "$rpart" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
prerelease_less_than "$(version_prerelease "$left")" "$(version_prerelease "$right")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_source_version() {
|
current_source_version() {
|
||||||
if [ -f Cargo.toml ]; then
|
if [ "$from_current" -eq 1 ]; then
|
||||||
|
[ -f Cargo.toml ] || return 1
|
||||||
sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p'
|
sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -329,32 +249,21 @@ current_source_version() {
|
|||||||
| sed -n '1p'
|
| sed -n '1p'
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_release_tag() {
|
release_source_ref() {
|
||||||
[ -n "$repo" ] || return 1
|
if [ -n "$binary_url" ] || [ -n "$binary_base" ]; then
|
||||||
web_base="$(repo_web_base "$repo")" || return 1
|
|
||||||
latest_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' "$web_base/releases/latest" 2>/dev/null || true)"
|
|
||||||
case "$latest_url" in
|
|
||||||
"$web_base"/releases/tag/*)
|
|
||||||
tag="${latest_url##"$web_base"/releases/tag/}"
|
|
||||||
[ -n "$tag" ] || return 1
|
|
||||||
printf '%s\n' "$tag"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
latest_release_is_stale() {
|
|
||||||
if [ -n "$binary_url" ] || [ -n "$binary_base" ] || [ "$binary_version" != "latest" ]; then
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
latest="$(latest_release_tag || true)"
|
if [ "$binary_version" != "latest" ] && [ "$binary_exact" = "1" ]; then
|
||||||
|
printf '%s\n' "$binary_version"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
current="$(current_source_version || true)"
|
current="$(current_source_version || true)"
|
||||||
[ -n "$latest" ] && [ -n "$current" ] || return 1
|
if [ -n "$current" ]; then
|
||||||
latest="${latest#v}"
|
printf 'v%s\n' "${current#v}"
|
||||||
if version_less_than "$latest" "$current"; then
|
return 0
|
||||||
echo "latest release $latest is older than source $current; skipping stale prebuilt" >&2
|
fi
|
||||||
|
if [ "$binary_version" != "latest" ]; then
|
||||||
|
printf '%s\n' "$binary_version"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
@@ -450,10 +359,8 @@ verify_archive_version() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try_install_prebuilt() {
|
try_install_prebuilt() {
|
||||||
if latest_release_is_stale; then
|
download_url="$(release_download_url)" || return 1
|
||||||
return 1
|
resolved_source_ref="$(release_source_ref || true)"
|
||||||
fi
|
|
||||||
download_url="$(release_latest_tag_download_url || release_download_url)" || return 1
|
|
||||||
tmpdir="$(mktemp -d)"
|
tmpdir="$(mktemp -d)"
|
||||||
archive="$tmpdir/$(release_artifact_name)"
|
archive="$tmpdir/$(release_artifact_name)"
|
||||||
checksum_file="$archive.sha256"
|
checksum_file="$archive.sha256"
|
||||||
@@ -490,7 +397,11 @@ try_install_prebuilt() {
|
|||||||
|
|
||||||
install_from_source() {
|
install_from_source() {
|
||||||
ensure_cargo
|
ensure_cargo
|
||||||
if [ "$from_current" -eq 1 ] || [ -f Cargo.toml ]; then
|
if [ "$from_current" -eq 1 ]; then
|
||||||
|
[ -f Cargo.toml ] || {
|
||||||
|
echo "--from-current requires a Dosh checkout with Cargo.toml" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
src_dir="$(pwd)"
|
src_dir="$(pwd)"
|
||||||
else
|
else
|
||||||
if [ -z "$repo" ]; then
|
if [ -z "$repo" ]; then
|
||||||
@@ -500,22 +411,27 @@ install_from_source() {
|
|||||||
update_cache="$(safe_update_cache_path "$update_cache")"
|
update_cache="$(safe_update_cache_path "$update_cache")"
|
||||||
need git
|
need git
|
||||||
mkdir -p "$(dirname "$update_cache")"
|
mkdir -p "$(dirname "$update_cache")"
|
||||||
|
source_ref="${resolved_source_ref:-main}"
|
||||||
if [ -d "$update_cache/.git" ]; then
|
if [ -d "$update_cache/.git" ]; then
|
||||||
[ "$quiet" != "1" ] && echo "Updating Dosh source"
|
[ "$quiet" != "1" ] && echo "Updating Dosh source"
|
||||||
git -C "$update_cache" remote set-url origin "$repo"
|
git -C "$update_cache" remote set-url origin "$repo"
|
||||||
if [ "$quiet" = "1" ]; then
|
if [ "$quiet" = "1" ]; then
|
||||||
git -C "$update_cache" fetch -q --depth 1 origin main
|
git -C "$update_cache" fetch -q --depth 1 origin "$source_ref"
|
||||||
else
|
else
|
||||||
git -C "$update_cache" fetch --depth 1 origin main
|
git -C "$update_cache" fetch --depth 1 origin "$source_ref"
|
||||||
|
fi
|
||||||
|
if [ "$source_ref" = "main" ]; then
|
||||||
|
git -C "$update_cache" checkout -q -B main FETCH_HEAD
|
||||||
|
else
|
||||||
|
git -C "$update_cache" checkout -q --detach FETCH_HEAD
|
||||||
fi
|
fi
|
||||||
git -C "$update_cache" checkout -q -B main FETCH_HEAD
|
|
||||||
else
|
else
|
||||||
[ "$quiet" != "1" ] && echo "Downloading Dosh source"
|
[ "$quiet" != "1" ] && echo "Downloading Dosh source"
|
||||||
rm -rf "$update_cache"
|
rm -rf "$update_cache"
|
||||||
if [ "$quiet" = "1" ]; then
|
if [ "$quiet" = "1" ]; then
|
||||||
git clone -q --depth 1 --branch main "$repo" "$update_cache" >/dev/null
|
git clone -q --depth 1 --branch "$source_ref" "$repo" "$update_cache" >/dev/null
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main "$repo" "$update_cache" >/dev/null
|
git clone --depth 1 --branch "$source_ref" "$repo" "$update_cache" >/dev/null
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
src_dir="$update_cache"
|
src_dir="$update_cache"
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
$version = "9.8.7-rc3"
|
||||||
|
$root = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-installer-test-" + [guid]::NewGuid())
|
||||||
|
$webRoot = Join-Path $root "web"
|
||||||
|
$repoRoot = Join-Path $webRoot "repo"
|
||||||
|
$releaseRoot = Join-Path $repoRoot "releases\download\v$version"
|
||||||
|
$rawRoot = Join-Path $repoRoot "raw\branch\main"
|
||||||
|
$stageRoot = Join-Path $root "stage\dosh"
|
||||||
|
$requestLog = Join-Path $root "requests.log"
|
||||||
|
$prefix = Join-Path $root "prefix"
|
||||||
|
$home = Join-Path $root "home"
|
||||||
|
$unrelated = Join-Path $root "unrelated"
|
||||||
|
$job = $null
|
||||||
|
$savedBinaryVersion = $env:DOSH_BINARY_VERSION
|
||||||
|
$savedBinaryExact = $env:DOSH_BINARY_EXACT
|
||||||
|
|
||||||
|
function Invoke-TestInstaller($HomePath, $WorkingDirectory, $InstallPrefix, $Repository) {
|
||||||
|
$savedHome = $env:HOME
|
||||||
|
$savedUserProfile = $env:USERPROFILE
|
||||||
|
try {
|
||||||
|
$env:HOME = $HomePath
|
||||||
|
$env:USERPROFILE = $HomePath
|
||||||
|
Push-Location $WorkingDirectory
|
||||||
|
try {
|
||||||
|
$powershell = (Get-Process -Id $PID).Path
|
||||||
|
& $powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\install.ps1") -Role client -Repo $Repository -Prefix $InstallPrefix -BinaryRequired
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Windows installer exited with status $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$env:HOME = $savedHome
|
||||||
|
$env:USERPROFILE = $savedUserProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Force -Path $releaseRoot, $rawRoot, (Join-Path $stageRoot "bin"), $home, $unrelated | Out-Null
|
||||||
|
@"
|
||||||
|
[package]
|
||||||
|
name = "dosh"
|
||||||
|
version = "$version"
|
||||||
|
"@ | Set-Content -Encoding ascii -NoNewline (Join-Path $rawRoot "Cargo.toml")
|
||||||
|
@"
|
||||||
|
[package]
|
||||||
|
name = "not-dosh"
|
||||||
|
version = "99.0.0"
|
||||||
|
"@ | Set-Content -Encoding ascii -NoNewline (Join-Path $unrelated "Cargo.toml")
|
||||||
|
"$version`n" | Set-Content -Encoding ascii -NoNewline (Join-Path $stageRoot "VERSION")
|
||||||
|
"fixture" | Set-Content -Encoding ascii -NoNewline (Join-Path $stageRoot "bin\dosh-client.exe")
|
||||||
|
|
||||||
|
$archive = Join-Path $releaseRoot "dosh-windows-x86_64.zip"
|
||||||
|
Compress-Archive -Force -Path $stageRoot -DestinationPath $archive
|
||||||
|
$digest = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
|
||||||
|
"$digest dosh-windows-x86_64.zip`n" | Set-Content -Encoding ascii -NoNewline "$archive.sha256"
|
||||||
|
|
||||||
|
$probe = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||||
|
$probe.Start()
|
||||||
|
$port = ([System.Net.IPEndPoint]$probe.LocalEndpoint).Port
|
||||||
|
$probe.Stop()
|
||||||
|
$listenPrefix = "http://127.0.0.1:$port/"
|
||||||
|
$job = Start-Job -ArgumentList $webRoot, $listenPrefix, $requestLog -ScriptBlock {
|
||||||
|
param($Root, $Prefix, $Log)
|
||||||
|
$listener = [System.Net.HttpListener]::new()
|
||||||
|
$listener.Prefixes.Add($Prefix)
|
||||||
|
$listener.Start()
|
||||||
|
try {
|
||||||
|
while ($listener.IsListening) {
|
||||||
|
$context = $listener.GetContext()
|
||||||
|
$path = [System.Uri]::UnescapeDataString($context.Request.Url.AbsolutePath.TrimStart('/'))
|
||||||
|
Add-Content -Encoding ascii -Path $Log -Value $context.Request.Url.AbsolutePath
|
||||||
|
$file = Join-Path $Root ($path.Replace('/', [System.IO.Path]::DirectorySeparatorChar))
|
||||||
|
if (Test-Path -LiteralPath $file -PathType Leaf) {
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($file)
|
||||||
|
$context.Response.StatusCode = 200
|
||||||
|
$context.Response.ContentLength64 = $bytes.Length
|
||||||
|
if ($context.Request.HttpMethod -ne "HEAD") {
|
||||||
|
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$context.Response.StatusCode = 404
|
||||||
|
}
|
||||||
|
$context.Response.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$listener.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ready = $false
|
||||||
|
for ($attempt = 0; $attempt -lt 50; $attempt++) {
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri "${listenPrefix}repo/raw/branch/main/Cargo.toml" | Out-Null
|
||||||
|
$ready = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $ready) {
|
||||||
|
throw "installer fixture HTTP server did not start"
|
||||||
|
}
|
||||||
|
|
||||||
|
$env:DOSH_BINARY_VERSION = "v0.1.17"
|
||||||
|
Remove-Item Env:DOSH_BINARY_EXACT -ErrorAction SilentlyContinue
|
||||||
|
Invoke-TestInstaller $home $unrelated $prefix "${listenPrefix}repo.git"
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $prefix "bin\dosh-client.exe"))) {
|
||||||
|
throw "Windows installer did not install dosh-client.exe"
|
||||||
|
}
|
||||||
|
$requests = Get-Content $requestLog -Raw
|
||||||
|
if ($requests -notmatch "/repo/raw/branch/main/Cargo.toml") {
|
||||||
|
throw "Windows installer did not read the repository release version"
|
||||||
|
}
|
||||||
|
if ($requests -notmatch "/repo/releases/download/v$([regex]::Escape($version))/dosh-windows-x86_64.zip") {
|
||||||
|
throw "Windows installer did not request the repository-declared release artifact"
|
||||||
|
}
|
||||||
|
if ($requests -match "/releases/latest" -or $requests -match "99.0.0") {
|
||||||
|
throw "Windows installer used a stable redirect or unrelated Cargo.toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
"fixture-v2" | Set-Content -Encoding ascii -NoNewline (Join-Path $stageRoot "bin\dosh-client.exe")
|
||||||
|
Compress-Archive -Force -Path $stageRoot -DestinationPath $archive
|
||||||
|
$digest = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
|
||||||
|
"$digest dosh-windows-x86_64.zip`n" | Set-Content -Encoding ascii -NoNewline "$archive.sha256"
|
||||||
|
|
||||||
|
$installedAlias = Join-Path $prefix "bin\dosh.exe"
|
||||||
|
$lock = [System.IO.File]::Open($installedAlias, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::None)
|
||||||
|
try {
|
||||||
|
Invoke-TestInstaller $home $unrelated $prefix "${listenPrefix}repo.git"
|
||||||
|
$pending = @(Get-ChildItem (Join-Path $prefix "bin") -Filter "dosh.exe.pending.*")
|
||||||
|
if ($pending.Count -ne 1) {
|
||||||
|
throw "locked Windows executable was not staged exactly once"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$lock.Dispose()
|
||||||
|
}
|
||||||
|
$replaced = $false
|
||||||
|
for ($attempt = 0; $attempt -lt 100; $attempt++) {
|
||||||
|
if ((Get-Content $installedAlias -Raw) -eq "fixture-v2" -and -not (Get-ChildItem (Join-Path $prefix "bin") -Filter "dosh.exe.pending.*")) {
|
||||||
|
$replaced = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
if (-not $replaced) {
|
||||||
|
throw "staged Windows executable was not applied after its lock was released"
|
||||||
|
}
|
||||||
|
Write-Host "Windows installer runtime test passed"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$env:DOSH_BINARY_VERSION = $savedBinaryVersion
|
||||||
|
$env:DOSH_BINARY_EXACT = $savedBinaryExact
|
||||||
|
if ($job) {
|
||||||
|
Stop-Job $job -ErrorAction SilentlyContinue
|
||||||
|
Remove-Job $job -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
+105
-162
@@ -4313,27 +4313,6 @@ fn update_installer_url(raw_base: &str, os: &str) -> String {
|
|||||||
format!("{raw_base}/raw/branch/main/{}", update_installer_name(os))
|
format!("{raw_base}/raw/branch/main/{}", update_installer_name(os))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
|
|
||||||
let web = repo
|
|
||||||
.strip_suffix(".git")
|
|
||||||
.unwrap_or(repo)
|
|
||||||
.trim_end_matches('/');
|
|
||||||
if !web.starts_with("http://") && !web.starts_with("https://") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(format!("{web}/releases/latest/download/{artifact}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_tag_from_effective_url(web: &str, effective: &str) -> Option<String> {
|
|
||||||
let prefix = format!("{web}/releases/tag/");
|
|
||||||
let tag = effective.strip_prefix(&prefix)?;
|
|
||||||
if tag.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(tag.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_version_from_tag(tag: &str) -> &str {
|
fn release_version_from_tag(tag: &str) -> &str {
|
||||||
tag.strip_prefix('v').unwrap_or(tag)
|
tag.strip_prefix('v').unwrap_or(tag)
|
||||||
}
|
}
|
||||||
@@ -4346,20 +4325,6 @@ fn update_version_status(local: &str, latest: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_binary_version_for_installer(local: &str, latest_tag: Option<&str>) -> Option<String> {
|
|
||||||
let latest = latest_tag.map(release_version_from_tag)?;
|
|
||||||
if compare_dotted_versions(latest, local) == std::cmp::Ordering::Less {
|
|
||||||
Some(format!("v{local}"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn effective_update_artifact_tag(local: &str, latest_tag: Option<&str>) -> Option<String> {
|
|
||||||
update_binary_version_for_installer(local, latest_tag)
|
|
||||||
.or_else(|| latest_tag.map(ToOwned::to_owned))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_dotted_versions(left: &str, right: &str) -> std::cmp::Ordering {
|
fn compare_dotted_versions(left: &str, right: &str) -> std::cmp::Ordering {
|
||||||
let left = parse_comparable_version(left);
|
let left = parse_comparable_version(left);
|
||||||
let right = parse_comparable_version(right);
|
let right = parse_comparable_version(right);
|
||||||
@@ -4447,7 +4412,7 @@ fn parse_dotted_version(version: &str) -> Vec<u64> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn latest_release_tag(repo: &str) -> Result<Option<String>> {
|
fn repository_release_tag(repo: &str) -> Result<Option<String>> {
|
||||||
let web = repo
|
let web = repo
|
||||||
.strip_suffix(".git")
|
.strip_suffix(".git")
|
||||||
.unwrap_or(repo)
|
.unwrap_or(repo)
|
||||||
@@ -4455,28 +4420,30 @@ fn latest_release_tag(repo: &str) -> Result<Option<String>> {
|
|||||||
if !web.starts_with("http://") && !web.starts_with("https://") {
|
if !web.starts_with("http://") && !web.starts_with("https://") {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let latest_url = format!("{web}/releases/latest");
|
let manifest_url = format!("{web}/raw/branch/main/Cargo.toml");
|
||||||
let output = if cfg!(windows) {
|
let output = if cfg!(windows) {
|
||||||
let script = windows_effective_url_script(&latest_url);
|
let script = windows_url_contents_script(&manifest_url);
|
||||||
windows_powershell_output(&script, &format!("resolve latest release for {web}"))?
|
windows_powershell_output(&script, &format!("read release version from {web}"))?
|
||||||
} else {
|
} else {
|
||||||
Command::new("curl")
|
Command::new("curl")
|
||||||
.arg("-fsSL")
|
.arg("-fsSL")
|
||||||
.arg("-o")
|
.arg(manifest_url)
|
||||||
.arg("/dev/null")
|
|
||||||
.arg("-w")
|
|
||||||
.arg("%{url_effective}")
|
|
||||||
.arg(latest_url)
|
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.output()
|
.output()
|
||||||
.with_context(|| format!("resolve latest release for {web}"))?
|
.with_context(|| format!("read release version from {web}"))?
|
||||||
};
|
};
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let effective = String::from_utf8_lossy(&output.stdout);
|
let manifest =
|
||||||
let effective = effective.trim();
|
String::from_utf8(output.stdout).context("repository Cargo.toml is not UTF-8")?;
|
||||||
Ok(release_tag_from_effective_url(web, effective))
|
let parsed: toml::Value = toml::from_str(&manifest).context("parse repository Cargo.toml")?;
|
||||||
|
let version = parsed
|
||||||
|
.get("package")
|
||||||
|
.and_then(|package| package.get("version"))
|
||||||
|
.and_then(toml::Value::as_str)
|
||||||
|
.filter(|version| !version.is_empty());
|
||||||
|
Ok(version.map(|version| format!("v{}", version.trim_start_matches('v'))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release_tag_download_url(repo: &str, tag: &str, artifact: &str) -> Option<String> {
|
fn release_tag_download_url(repo: &str, tag: &str, artifact: &str) -> Option<String> {
|
||||||
@@ -4524,7 +4491,7 @@ fn run_update(
|
|||||||
let update_remote_server = options.role.updates_remote_server_from_os(os);
|
let update_remote_server = options.role.updates_remote_server_from_os(os);
|
||||||
if options.check_only {
|
if options.check_only {
|
||||||
let local_version = env!("CARGO_PKG_VERSION");
|
let local_version = env!("CARGO_PKG_VERSION");
|
||||||
let latest_tag = latest_release_tag(&repo)?;
|
let release_tag = repository_release_tag(&repo)?;
|
||||||
println!("dosh {local_version}");
|
println!("dosh {local_version}");
|
||||||
println!("repo: {repo}");
|
println!("repo: {repo}");
|
||||||
if let Some(local_installer_role) = local_installer_role {
|
if let Some(local_installer_role) = local_installer_role {
|
||||||
@@ -4538,7 +4505,7 @@ fn run_update(
|
|||||||
);
|
);
|
||||||
println!("remote_server: {}", config.server);
|
println!("remote_server: {}", config.server);
|
||||||
}
|
}
|
||||||
match latest_tag.as_deref() {
|
match release_tag.as_deref() {
|
||||||
Some(tag) => {
|
Some(tag) => {
|
||||||
let latest_version = release_version_from_tag(tag);
|
let latest_version = release_version_from_tag(tag);
|
||||||
println!(
|
println!(
|
||||||
@@ -4553,22 +4520,14 @@ fn run_update(
|
|||||||
update_remote_server,
|
update_remote_server,
|
||||||
&repo,
|
&repo,
|
||||||
&artifact,
|
&artifact,
|
||||||
|
release_tag.as_deref(),
|
||||||
) {
|
) {
|
||||||
LocalPrebuiltCheckTarget::Url(latest_url) => {
|
LocalPrebuiltCheckTarget::Url(display_url) => {
|
||||||
let mut status = "missing";
|
let status = if url_reachable(&display_url)? {
|
||||||
let mut display_url = latest_url.clone();
|
"available"
|
||||||
if let Some(tag_url) =
|
} else {
|
||||||
effective_update_artifact_tag(local_version, latest_tag.as_deref())
|
"missing; source fallback will build the same tag"
|
||||||
.as_deref()
|
};
|
||||||
.and_then(|tag| release_tag_download_url(&repo, tag, &artifact))
|
|
||||||
{
|
|
||||||
display_url = tag_url;
|
|
||||||
if url_reachable(&display_url)? {
|
|
||||||
status = "available";
|
|
||||||
}
|
|
||||||
} else if url_reachable(&latest_url)? {
|
|
||||||
status = "available";
|
|
||||||
}
|
|
||||||
println!("prebuilt: {status} ({artifact})");
|
println!("prebuilt: {status} ({artifact})");
|
||||||
println!("prebuilt_url: {display_url}");
|
println!("prebuilt_url: {display_url}");
|
||||||
}
|
}
|
||||||
@@ -4578,6 +4537,9 @@ fn run_update(
|
|||||||
LocalPrebuiltCheckTarget::RemoteOnly => {
|
LocalPrebuiltCheckTarget::RemoteOnly => {
|
||||||
println!("prebuilt: skipped for remote-only update");
|
println!("prebuilt: skipped for remote-only update");
|
||||||
}
|
}
|
||||||
|
LocalPrebuiltCheckTarget::ReleaseUnknown => {
|
||||||
|
println!("prebuilt: unavailable; repository release version is unknown");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -4586,10 +4548,7 @@ fn run_update(
|
|||||||
.join("dosh")
|
.join("dosh")
|
||||||
.join("source");
|
.join("source");
|
||||||
let use_prebuilt = std::env::var("DOSH_USE_PREBUILT").unwrap_or_else(|_| "1".to_string());
|
let use_prebuilt = std::env::var("DOSH_USE_PREBUILT").unwrap_or_else(|_| "1".to_string());
|
||||||
let binary_version = update_binary_version_for_installer(
|
let binary_version = repository_release_tag(&repo)?;
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
latest_release_tag(&repo)?.as_deref(),
|
|
||||||
);
|
|
||||||
if update_remote_server {
|
if update_remote_server {
|
||||||
run_remote_server_update(
|
run_remote_server_update(
|
||||||
config,
|
config,
|
||||||
@@ -4619,6 +4578,7 @@ enum LocalPrebuiltCheckTarget {
|
|||||||
Url(String),
|
Url(String),
|
||||||
NonHttpRepo,
|
NonHttpRepo,
|
||||||
RemoteOnly,
|
RemoteOnly,
|
||||||
|
ReleaseUnknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_prebuilt_check_target(
|
fn local_prebuilt_check_target(
|
||||||
@@ -4626,6 +4586,7 @@ fn local_prebuilt_check_target(
|
|||||||
update_remote_server: bool,
|
update_remote_server: bool,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
artifact: &str,
|
artifact: &str,
|
||||||
|
release_tag: Option<&str>,
|
||||||
) -> LocalPrebuiltCheckTarget {
|
) -> LocalPrebuiltCheckTarget {
|
||||||
if local_installer_role.is_none() {
|
if local_installer_role.is_none() {
|
||||||
return if update_remote_server {
|
return if update_remote_server {
|
||||||
@@ -4634,7 +4595,10 @@ fn local_prebuilt_check_target(
|
|||||||
LocalPrebuiltCheckTarget::NonHttpRepo
|
LocalPrebuiltCheckTarget::NonHttpRepo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
latest_release_download_url(repo, artifact)
|
let Some(release_tag) = release_tag else {
|
||||||
|
return LocalPrebuiltCheckTarget::ReleaseUnknown;
|
||||||
|
};
|
||||||
|
release_tag_download_url(repo, release_tag, artifact)
|
||||||
.map(LocalPrebuiltCheckTarget::Url)
|
.map(LocalPrebuiltCheckTarget::Url)
|
||||||
.unwrap_or(LocalPrebuiltCheckTarget::NonHttpRepo)
|
.unwrap_or(LocalPrebuiltCheckTarget::NonHttpRepo)
|
||||||
}
|
}
|
||||||
@@ -4727,6 +4691,7 @@ fn unix_update_script(
|
|||||||
);
|
);
|
||||||
if let Some(version) = binary_version {
|
if let Some(version) = binary_version {
|
||||||
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
|
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
|
||||||
|
env.push_str(" DOSH_BINARY_EXACT=1");
|
||||||
}
|
}
|
||||||
let mut script = format!(
|
let mut script = format!(
|
||||||
"curl -fsSL {} | {} sh -s -- {}",
|
"curl -fsSL {} | {} sh -s -- {}",
|
||||||
@@ -4758,6 +4723,7 @@ fn remote_unix_server_update_script(
|
|||||||
);
|
);
|
||||||
if let Some(version) = binary_version {
|
if let Some(version) = binary_version {
|
||||||
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
|
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
|
||||||
|
env.push_str(" DOSH_BINARY_EXACT=1");
|
||||||
}
|
}
|
||||||
format!(
|
format!(
|
||||||
"curl -fsSL {} | {} sh -s -- server",
|
"curl -fsSL {} | {} sh -s -- server",
|
||||||
@@ -4795,6 +4761,7 @@ fn windows_update_script(
|
|||||||
"$env:DOSH_BINARY_VERSION={};",
|
"$env:DOSH_BINARY_VERSION={};",
|
||||||
powershell_string(version)
|
powershell_string(version)
|
||||||
));
|
));
|
||||||
|
script.push_str("$env:DOSH_BINARY_EXACT='1';");
|
||||||
}
|
}
|
||||||
if config.server != "user@example.com" {
|
if config.server != "user@example.com" {
|
||||||
script.push_str(&format!(
|
script.push_str(&format!(
|
||||||
@@ -4809,19 +4776,12 @@ fn windows_update_script(
|
|||||||
script
|
script
|
||||||
}
|
}
|
||||||
|
|
||||||
fn windows_effective_url_script(url: &str) -> String {
|
fn windows_url_contents_script(url: &str) -> String {
|
||||||
let url = powershell_string(url);
|
let url = powershell_string(url);
|
||||||
format!(
|
format!(
|
||||||
"$ErrorActionPreference='Stop';\
|
"$ErrorActionPreference='Stop';\
|
||||||
$ProgressPreference='SilentlyContinue';\
|
$ProgressPreference='SilentlyContinue';\
|
||||||
$response=Invoke-WebRequest -UseBasicParsing -Uri {url} -MaximumRedirection 5;\
|
(Invoke-WebRequest -UseBasicParsing -Uri {url} -MaximumRedirection 5).Content"
|
||||||
if ($response.BaseResponse.ResponseUri) {{ \
|
|
||||||
$response.BaseResponse.ResponseUri.AbsoluteUri \
|
|
||||||
}} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {{ \
|
|
||||||
$response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri \
|
|
||||||
}} else {{ \
|
|
||||||
{url} \
|
|
||||||
}}"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10950,39 +10910,38 @@ mod tests {
|
|||||||
SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_CLEANUP,
|
SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_CLEANUP,
|
||||||
TERMINAL_SNAPSHOT_RESET, UpdateOptions, UpdateRole, auth_allows, cache_key,
|
TERMINAL_SNAPSHOT_RESET, UpdateOptions, UpdateRole, auth_allows, cache_key,
|
||||||
cache_server_prefix, cleanup_stream_state, clear_cached_credentials,
|
cache_server_prefix, cleanup_stream_state, clear_cached_credentials,
|
||||||
effective_update_artifact_tag, ensure_tui_safe_status_overlay, expand_ssh_path_tokens,
|
ensure_tui_safe_status_overlay, expand_ssh_path_tokens, first_resolved_addr,
|
||||||
first_resolved_addr, imported_host_block, input_contains_focus_in, input_matches_escape,
|
imported_host_block, input_contains_focus_in, input_matches_escape, is_local_status_target,
|
||||||
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
is_resume_response_for_client, load_first_native_identity_with_prompt,
|
||||||
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
|
local_symlink_target_is_dir, local_username_from_env, native_proxy_udp_warning,
|
||||||
local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from,
|
newest_client_trace_path_from, note_authenticated_contact, note_snapshot_rendered,
|
||||||
note_authenticated_contact, note_snapshot_rendered, parse_dynamic_forward,
|
parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward,
|
||||||
parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path,
|
parse_single_remote_path, parse_ssh_config, parse_trace_line, parse_trace_options,
|
||||||
parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options,
|
parse_trace_report_options, parse_trace_summary, parse_update_options,
|
||||||
parse_trace_summary, parse_update_options, post_submit_hold_duration,
|
post_submit_hold_duration, queue_or_send_stream_data, queue_pending_user_input,
|
||||||
queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input,
|
queue_stale_pending_user_input, raw_contains_host_table, recv_response_until,
|
||||||
raw_contains_host_table, recv_response_until, refresh_live_addr, release_artifact_name_for,
|
refresh_live_addr, release_artifact_name_for, release_tag_download_url,
|
||||||
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
|
release_version_from_tag, remote_unix_server_update_script, render_frame_bytes,
|
||||||
remote_unix_server_update_script, render_frame_bytes, render_status_overlay, requested_env,
|
render_status_overlay, requested_env, resolve_forward_agent_endpoint,
|
||||||
resolve_forward_agent_endpoint, resolved_startup_command, retire_stream_state,
|
resolved_startup_command, retire_stream_state, retransmit_stream_closes,
|
||||||
retransmit_stream_closes, retransmit_stream_eofs, retransmit_stream_opens,
|
retransmit_stream_eofs, retransmit_stream_opens, retransmit_stream_window_adjusts,
|
||||||
retransmit_stream_window_adjusts, rewrite_forward_command, sanitize_trace_name,
|
rewrite_forward_command, sanitize_trace_name, selected_predict_mode, selected_udp_host,
|
||||||
selected_predict_mode, selected_udp_host, send_stream_eof, server_version_mismatch,
|
send_stream_eof, server_version_mismatch, should_flush_terminal_input_after_contact,
|
||||||
should_flush_terminal_input_after_contact, should_health_log_client_start,
|
should_health_log_client_start, should_hold_during_startup_gate,
|
||||||
should_hold_during_startup_gate, should_hold_post_submit_input,
|
should_hold_post_submit_input, should_reconnect_before_input_for_local_sleep,
|
||||||
should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal,
|
should_repaint_idle_terminal, should_strip_unowned_terminal_reports, socket_bind_display,
|
||||||
should_strip_unowned_terminal_reports, socket_bind_display, split_after_command_submit,
|
split_after_command_submit, split_forward_spec, split_trace_tokens, ssh_command_target,
|
||||||
split_forward_spec, split_trace_tokens, ssh_command_target, ssh_config_uses_proxy,
|
ssh_config_uses_proxy, ssh_config_word_for_os, ssh_destination_host, ssh_username,
|
||||||
ssh_config_word_for_os, ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
|
||||||
status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports,
|
strip_terminal_focus_reports, strip_unowned_terminal_reports, summarize_trace_file,
|
||||||
strip_unowned_terminal_reports, summarize_trace_file, summarize_trace_file_with_mode,
|
summarize_trace_file_with_mode, terminal_private_mode_transition, toml_bare_key_or_quoted,
|
||||||
terminal_private_mode_transition, toml_bare_key_or_quoted, top_trace_events,
|
top_trace_events, trace_report_warnings, unix_update_script, update_installer_url,
|
||||||
trace_report_warnings, unix_update_script, update_binary_version_for_installer,
|
update_version_status, upsert_managed_block, valid_forward_host, vscode_command_candidates,
|
||||||
update_installer_url, update_version_status, upsert_managed_block, valid_forward_host,
|
vscode_fallback_command, vscode_safe_alias, wake_repaint_retry_deadline,
|
||||||
vscode_command_candidates, vscode_fallback_command, vscode_safe_alias,
|
windows_command_word, windows_deferred_update_script, windows_mode_from_readonly,
|
||||||
wake_repaint_retry_deadline, windows_command_word, windows_deferred_update_script,
|
|
||||||
windows_effective_url_script, windows_mode_from_readonly,
|
|
||||||
windows_powershell_command_candidates, windows_readonly_from_mode, windows_update_script,
|
windows_powershell_command_candidates, windows_readonly_from_mode, windows_update_script,
|
||||||
windows_url_reachable_script, windows_vt_input_mode, windows_vt_output_mode,
|
windows_url_contents_script, windows_url_reachable_script, windows_vt_input_mode,
|
||||||
|
windows_vt_output_mode,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -13421,9 +13380,10 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
"https://git.palav.dev/Palav/dosh.git",
|
"https://git.palav.dev/Palav/dosh.git",
|
||||||
"dosh-windows-x86_64.zip",
|
"dosh-windows-x86_64.zip",
|
||||||
|
Some("v1.0.0-rc42"),
|
||||||
),
|
),
|
||||||
super::LocalPrebuiltCheckTarget::Url(
|
super::LocalPrebuiltCheckTarget::Url(
|
||||||
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-windows-x86_64.zip"
|
"https://git.palav.dev/Palav/dosh/releases/download/v1.0.0-rc42/dosh-windows-x86_64.zip"
|
||||||
.to_string()
|
.to_string()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -13433,6 +13393,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
"git@git.palav.dev:Palav/dosh.git",
|
"git@git.palav.dev:Palav/dosh.git",
|
||||||
"dosh-windows-x86_64.zip",
|
"dosh-windows-x86_64.zip",
|
||||||
|
Some("v1.0.0-rc42"),
|
||||||
),
|
),
|
||||||
super::LocalPrebuiltCheckTarget::NonHttpRepo
|
super::LocalPrebuiltCheckTarget::NonHttpRepo
|
||||||
);
|
);
|
||||||
@@ -13442,9 +13403,20 @@ mod tests {
|
|||||||
UpdateRole::Server.updates_remote_server_from_os("windows"),
|
UpdateRole::Server.updates_remote_server_from_os("windows"),
|
||||||
"https://git.palav.dev/Palav/dosh.git",
|
"https://git.palav.dev/Palav/dosh.git",
|
||||||
"dosh-windows-x86_64.zip",
|
"dosh-windows-x86_64.zip",
|
||||||
|
Some("v1.0.0-rc42"),
|
||||||
),
|
),
|
||||||
super::LocalPrebuiltCheckTarget::RemoteOnly
|
super::LocalPrebuiltCheckTarget::RemoteOnly
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::local_prebuilt_check_target(
|
||||||
|
Some("client"),
|
||||||
|
false,
|
||||||
|
"https://git.palav.dev/Palav/dosh.git",
|
||||||
|
"dosh-windows-x86_64.zip",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
super::LocalPrebuiltCheckTarget::ReleaseUnknown
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -13552,12 +13524,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windows_update_probes_are_powershell_native() {
|
fn windows_update_probes_are_powershell_native() {
|
||||||
let latest =
|
let manifest = windows_url_contents_script(
|
||||||
windows_effective_url_script("https://git.palav.dev/Palav/dosh/releases/latest");
|
"https://git.palav.dev/Palav/dosh/raw/branch/main/Cargo.toml",
|
||||||
assert!(latest.contains("Invoke-WebRequest -UseBasicParsing"));
|
);
|
||||||
assert!(latest.contains("-MaximumRedirection 5"));
|
assert!(manifest.contains("Invoke-WebRequest -UseBasicParsing"));
|
||||||
assert!(latest.contains("$response.BaseResponse.ResponseUri.AbsoluteUri"));
|
assert!(manifest.contains("-MaximumRedirection 5"));
|
||||||
assert!(!latest.contains("curl"));
|
assert!(manifest.contains(".Content"));
|
||||||
|
assert!(!manifest.contains("curl"));
|
||||||
|
|
||||||
let reachable = windows_url_reachable_script(
|
let reachable = windows_url_reachable_script(
|
||||||
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-windows-x86_64.zip",
|
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-windows-x86_64.zip",
|
||||||
@@ -13578,36 +13551,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_uses_local_tag_when_release_latest_is_older() {
|
fn update_pins_the_repository_release_for_every_platform() {
|
||||||
assert_eq!(
|
|
||||||
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc37")).as_deref(),
|
|
||||||
Some("v1.0.0-rc41")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc41")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc42")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update_binary_version_for_installer("1.0.0", Some("v1.0.0-rc42")).as_deref(),
|
|
||||||
Some("v1.0.0")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update_binary_version_for_installer("1.0.0-rc42", Some("v1.0.0")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
effective_update_artifact_tag("1.0.0-rc41", Some("v1.0.0-rc37")).as_deref(),
|
|
||||||
Some("v1.0.0-rc41")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
effective_update_artifact_tag("1.0.0-rc41", Some("v1.0.0-rc42")).as_deref(),
|
|
||||||
Some("v1.0.0-rc42")
|
|
||||||
);
|
|
||||||
assert_eq!(effective_update_artifact_tag("1.0.0-rc41", None), None);
|
|
||||||
let config = ClientConfig::default();
|
let config = ClientConfig::default();
|
||||||
let unix = unix_update_script(
|
let unix = unix_update_script(
|
||||||
&config,
|
&config,
|
||||||
@@ -13619,6 +13563,7 @@ mod tests {
|
|||||||
Some("v1.0.0-rc41"),
|
Some("v1.0.0-rc41"),
|
||||||
);
|
);
|
||||||
assert!(unix.contains("DOSH_BINARY_VERSION='v1.0.0-rc41'"));
|
assert!(unix.contains("DOSH_BINARY_VERSION='v1.0.0-rc41'"));
|
||||||
|
assert!(unix.contains("DOSH_BINARY_EXACT=1"));
|
||||||
assert!(unix.contains("| DOSH_REPO="));
|
assert!(unix.contains("| DOSH_REPO="));
|
||||||
assert!(unix.contains(" sh -s -- 'both'"));
|
assert!(unix.contains(" sh -s -- 'both'"));
|
||||||
|
|
||||||
@@ -13632,6 +13577,7 @@ mod tests {
|
|||||||
Some("v1.0.0-rc41"),
|
Some("v1.0.0-rc41"),
|
||||||
);
|
);
|
||||||
assert!(windows.contains("$env:DOSH_BINARY_VERSION='v1.0.0-rc41';"));
|
assert!(windows.contains("$env:DOSH_BINARY_VERSION='v1.0.0-rc41';"));
|
||||||
|
assert!(windows.contains("$env:DOSH_BINARY_EXACT='1';"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -13687,17 +13633,23 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn release_download_url_uses_latest_release_asset() {
|
fn release_download_url_uses_explicit_repository_release() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
latest_release_download_url(
|
release_tag_download_url(
|
||||||
"https://git.palav.dev/Palav/dosh.git",
|
"https://git.palav.dev/Palav/dosh.git",
|
||||||
|
"v1.0.0-rc42",
|
||||||
"dosh-linux-x86_64.tar.gz"
|
"dosh-linux-x86_64.tar.gz"
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-linux-x86_64.tar.gz"
|
"https://git.palav.dev/Palav/dosh/releases/download/v1.0.0-rc42/dosh-linux-x86_64.tar.gz"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
latest_release_download_url("git@git.palav.dev:Palav/dosh.git", "artifact").is_none()
|
release_tag_download_url(
|
||||||
|
"git@git.palav.dev:Palav/dosh.git",
|
||||||
|
"v1.0.0-rc42",
|
||||||
|
"artifact"
|
||||||
|
)
|
||||||
|
.is_none()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13726,15 +13678,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn release_tag_parses_effective_latest_url() {
|
fn release_tag_builds_exact_artifact_url() {
|
||||||
assert_eq!(
|
|
||||||
release_tag_from_effective_url(
|
|
||||||
"https://example.com/owner/dosh",
|
|
||||||
"https://example.com/owner/dosh/releases/tag/v1.0.0"
|
|
||||||
)
|
|
||||||
.as_deref(),
|
|
||||||
Some("v1.0.0")
|
|
||||||
);
|
|
||||||
assert_eq!(release_version_from_tag("v1.0.0"), "1.0.0");
|
assert_eq!(release_version_from_tag("v1.0.0"), "1.0.0");
|
||||||
assert_eq!(release_version_from_tag("2026.06.28"), "2026.06.28");
|
assert_eq!(release_version_from_tag("2026.06.28"), "2026.06.28");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -13748,7 +13692,6 @@ mod tests {
|
|||||||
"https://example.com/owner/dosh/releases/download/v1.0.0/dosh-linux-x86_64.tar.gz"
|
"https://example.com/owner/dosh/releases/download/v1.0.0/dosh-linux-x86_64.tar.gz"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert!(release_tag_from_effective_url("https://example.com/owner/dosh", "").is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#![cfg(unix)]
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
const VERSION: &str = "9.8.7-rc3";
|
||||||
|
|
||||||
|
fn write_executable(path: &Path, body: &str) {
|
||||||
|
fs::write(path, body).unwrap();
|
||||||
|
let mut permissions = fs::metadata(path).unwrap().permissions();
|
||||||
|
permissions.set_mode(0o755);
|
||||||
|
fs::set_permissions(path, permissions).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstallerFixture {
|
||||||
|
root: TempDir,
|
||||||
|
fake_bin: PathBuf,
|
||||||
|
archive: PathBuf,
|
||||||
|
checksum: PathBuf,
|
||||||
|
curl_log: PathBuf,
|
||||||
|
git_log: PathBuf,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstallerFixture {
|
||||||
|
fn new(version: &str) -> Self {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let fake_bin = root.path().join("fake-bin");
|
||||||
|
let stage = root.path().join("stage/dosh");
|
||||||
|
fs::create_dir_all(fake_bin.as_path()).unwrap();
|
||||||
|
fs::create_dir_all(stage.join("bin")).unwrap();
|
||||||
|
fs::write(stage.join("VERSION"), format!("{version}\n")).unwrap();
|
||||||
|
write_executable(
|
||||||
|
&stage.join("bin/dosh-client"),
|
||||||
|
"#!/bin/sh\nprintf 'fixture dosh\\n'\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let archive = root.path().join("dosh-macos-aarch64.tar.gz");
|
||||||
|
let status = Command::new("tar")
|
||||||
|
.args(["-C", root.path().join("stage").to_str().unwrap(), "-czf"])
|
||||||
|
.arg(&archive)
|
||||||
|
.arg("dosh")
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(status.success());
|
||||||
|
let digest = Sha256::digest(fs::read(&archive).unwrap());
|
||||||
|
let checksum = root.path().join("dosh-macos-aarch64.tar.gz.sha256");
|
||||||
|
fs::write(
|
||||||
|
&checksum,
|
||||||
|
format!("{digest:x} dosh-macos-aarch64.tar.gz\n"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
write_executable(
|
||||||
|
&fake_bin.join("uname"),
|
||||||
|
"#!/bin/sh\ncase \"${1:-}\" in -s) echo Darwin;; -m) echo arm64;; *) echo Darwin;; esac\n",
|
||||||
|
);
|
||||||
|
let curl_log = root.path().join("curl.log");
|
||||||
|
let git_log = root.path().join("git.log");
|
||||||
|
write_executable(
|
||||||
|
&fake_bin.join("curl"),
|
||||||
|
r#"#!/bin/sh
|
||||||
|
printf '%s\n' "$*" >>"$TEST_CURL_LOG"
|
||||||
|
out=
|
||||||
|
previous=
|
||||||
|
url=
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$previous" = "-o" ]; then out="$arg"; fi
|
||||||
|
previous="$arg"
|
||||||
|
case "$arg" in http://*|https://*) url="$arg";; esac
|
||||||
|
done
|
||||||
|
case "$url" in
|
||||||
|
*/raw/branch/main/Cargo.toml)
|
||||||
|
printf '[package]\nname = "dosh"\nversion = "%s"\n' "$TEST_RELEASE_VERSION"
|
||||||
|
;;
|
||||||
|
*.sha256)
|
||||||
|
cp "$TEST_ARCHIVE_CHECKSUM" "$out"
|
||||||
|
;;
|
||||||
|
*/releases/download/*)
|
||||||
|
[ "${TEST_FAIL_ARCHIVE:-0}" = 1 ] && exit 22
|
||||||
|
cp "$TEST_ARCHIVE" "$out"
|
||||||
|
;;
|
||||||
|
*) exit 22;;
|
||||||
|
esac
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
root,
|
||||||
|
fake_bin,
|
||||||
|
archive,
|
||||||
|
checksum,
|
||||||
|
curl_log,
|
||||||
|
git_log,
|
||||||
|
version: version.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command(&self, cwd: &Path) -> Command {
|
||||||
|
let mut command = Command::new("sh");
|
||||||
|
command
|
||||||
|
.arg(format!("{}/install.sh", env!("CARGO_MANIFEST_DIR")))
|
||||||
|
.args([
|
||||||
|
"client",
|
||||||
|
"--repo",
|
||||||
|
"https://example.invalid/Palav/dosh.git",
|
||||||
|
"--prefix",
|
||||||
|
])
|
||||||
|
.arg(self.root.path().join("prefix"))
|
||||||
|
.current_dir(cwd)
|
||||||
|
.env("HOME", self.root.path().join("home"))
|
||||||
|
.env(
|
||||||
|
"PATH",
|
||||||
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
self.fake_bin.display(),
|
||||||
|
std::env::var("PATH").unwrap_or_default()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.env("DOSH_BINARY_REQUIRED", "1")
|
||||||
|
.env("TEST_RELEASE_VERSION", &self.version)
|
||||||
|
.env("TEST_ARCHIVE", &self.archive)
|
||||||
|
.env("TEST_ARCHIVE_CHECKSUM", &self.checksum)
|
||||||
|
.env("TEST_CURL_LOG", &self.curl_log)
|
||||||
|
.env("TEST_GIT_LOG", &self.git_log);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn installed_client(&self) -> PathBuf {
|
||||||
|
self.root.path().join("prefix/bin/dosh-client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unix_installer_uses_repository_version_even_inside_an_unrelated_rust_project() {
|
||||||
|
let fixture = InstallerFixture::new(VERSION);
|
||||||
|
let unrelated = fixture.root.path().join("unrelated");
|
||||||
|
fs::create_dir_all(&unrelated).unwrap();
|
||||||
|
fs::write(
|
||||||
|
unrelated.join("Cargo.toml"),
|
||||||
|
"[package]\nname = \"not-dosh\"\nversion = \"99.0.0\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output = fixture.command(&unrelated).output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"installer failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert!(fixture.installed_client().is_file());
|
||||||
|
let requests = fs::read_to_string(&fixture.curl_log).unwrap();
|
||||||
|
assert!(requests.contains("/raw/branch/main/Cargo.toml"));
|
||||||
|
assert!(requests.contains(&format!(
|
||||||
|
"/releases/download/v{VERSION}/dosh-macos-aarch64.tar.gz"
|
||||||
|
)));
|
||||||
|
assert!(!requests.contains("/releases/latest"));
|
||||||
|
assert!(!requests.contains("99.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unix_installer_source_fallback_checks_out_the_same_release_tag() {
|
||||||
|
let fixture = InstallerFixture::new(VERSION);
|
||||||
|
write_executable(
|
||||||
|
&fixture.fake_bin.join("git"),
|
||||||
|
r#"#!/bin/sh
|
||||||
|
printf '%s\n' "$*" >>"$TEST_GIT_LOG"
|
||||||
|
if [ "${1:-}" = clone ]; then
|
||||||
|
for destination in "$@"; do :; done
|
||||||
|
mkdir -p "$destination/.git"
|
||||||
|
fi
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
write_executable(
|
||||||
|
&fixture.fake_bin.join("cargo"),
|
||||||
|
"#!/bin/sh\nmkdir -p target/release\nprintf '#!/bin/sh\\n' >target/release/dosh-client\nchmod +x target/release/dosh-client\n",
|
||||||
|
);
|
||||||
|
let cwd = fixture.root.path().join("plain");
|
||||||
|
fs::create_dir_all(&cwd).unwrap();
|
||||||
|
let mut command = fixture.command(&cwd);
|
||||||
|
command
|
||||||
|
.env("DOSH_BINARY_REQUIRED", "0")
|
||||||
|
.env("TEST_FAIL_ARCHIVE", "1");
|
||||||
|
|
||||||
|
let output = command.output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"installer failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert!(fixture.installed_client().is_file());
|
||||||
|
let git = fs::read_to_string(&fixture.git_log).unwrap();
|
||||||
|
assert!(git.contains(&format!("clone --depth 1 --branch v{VERSION}")));
|
||||||
|
assert!(!git.contains("--branch main"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unix_installer_resolves_a_stable_repository_version_without_latest_redirects() {
|
||||||
|
let version = "1.2.3";
|
||||||
|
let fixture = InstallerFixture::new(version);
|
||||||
|
let cwd = fixture.root.path().join("plain");
|
||||||
|
fs::create_dir_all(&cwd).unwrap();
|
||||||
|
|
||||||
|
let output = fixture.command(&cwd).output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"installer failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let requests = fs::read_to_string(&fixture.curl_log).unwrap();
|
||||||
|
assert!(requests.contains(&format!(
|
||||||
|
"/releases/download/v{version}/dosh-macos-aarch64.tar.gz"
|
||||||
|
)));
|
||||||
|
assert!(!requests.contains("/releases/latest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unix_installer_repairs_a_legacy_updater_that_requests_its_old_tag() {
|
||||||
|
let fixture = InstallerFixture::new(VERSION);
|
||||||
|
let cwd = fixture.root.path().join("plain");
|
||||||
|
fs::create_dir_all(&cwd).unwrap();
|
||||||
|
let mut command = fixture.command(&cwd);
|
||||||
|
command.env("DOSH_BINARY_VERSION", "v0.1.17");
|
||||||
|
|
||||||
|
let output = command.output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"installer failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let requests = fs::read_to_string(&fixture.curl_log).unwrap();
|
||||||
|
assert!(requests.contains(&format!(
|
||||||
|
"/releases/download/v{VERSION}/dosh-macos-aarch64.tar.gz"
|
||||||
|
)));
|
||||||
|
assert!(!requests.contains("/releases/download/v0.1.17/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unix_installer_honors_an_explicit_exact_release_pin() {
|
||||||
|
let pinned = "0.1.17";
|
||||||
|
let fixture = InstallerFixture::new(pinned);
|
||||||
|
let cwd = fixture.root.path().join("plain");
|
||||||
|
fs::create_dir_all(&cwd).unwrap();
|
||||||
|
let mut command = fixture.command(&cwd);
|
||||||
|
command
|
||||||
|
.env("TEST_RELEASE_VERSION", VERSION)
|
||||||
|
.env("DOSH_BINARY_VERSION", format!("v{pinned}"))
|
||||||
|
.env("DOSH_BINARY_EXACT", "1");
|
||||||
|
|
||||||
|
let output = command.output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"installer failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let requests = fs::read_to_string(&fixture.curl_log).unwrap();
|
||||||
|
assert!(requests.contains(&format!(
|
||||||
|
"/releases/download/v{pinned}/dosh-macos-aarch64.tar.gz"
|
||||||
|
)));
|
||||||
|
assert!(!requests.contains(&format!("/releases/download/v{VERSION}/")));
|
||||||
|
}
|
||||||
+26
-23
@@ -94,6 +94,8 @@ fn release_gates_test_all_targets() {
|
|||||||
assert!(windows_client.contains("runs-on: windows-latest"));
|
assert!(windows_client.contains("runs-on: windows-latest"));
|
||||||
assert!(windows_client.contains("name: Test Windows client"));
|
assert!(windows_client.contains("name: Test Windows client"));
|
||||||
assert!(windows_client.contains("name: PowerShell syntax check"));
|
assert!(windows_client.contains("name: PowerShell syntax check"));
|
||||||
|
assert!(windows_client.contains("name: Windows installer runtime test"));
|
||||||
|
assert!(windows_client.contains("run: scripts/test-install-windows.ps1"));
|
||||||
assert!(windows_client.contains("shell: pwsh"));
|
assert!(windows_client.contains("shell: pwsh"));
|
||||||
assert!(
|
assert!(
|
||||||
windows_client
|
windows_client
|
||||||
@@ -138,28 +140,24 @@ fn one_dot_zero_gates_default_to_full_length_soaks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn installers_skip_stale_latest_release_prebuilts() {
|
fn installers_resolve_the_repository_declared_release_without_stable_redirects() {
|
||||||
let install = include_str!("../install.sh");
|
let install = include_str!("../install.sh");
|
||||||
assert!(install.contains("latest_release_is_stale"));
|
|
||||||
assert!(install.contains("current_source_version"));
|
assert!(install.contains("current_source_version"));
|
||||||
assert!(install.contains("latest release $latest is older than source $current"));
|
assert!(install.contains("release_source_ref"));
|
||||||
assert!(install.contains("version_prerelease"));
|
assert!(install.contains("printf 'v%s\\n' \"${current#v}\""));
|
||||||
assert!(install.contains("prerelease_less_than"));
|
assert!(install.contains("DOSH_BINARY_EXACT=1"));
|
||||||
assert!(
|
assert!(!install.contains("/releases/latest"));
|
||||||
install.contains("if latest_release_is_stale; then"),
|
assert!(install.contains("resolved_source_ref=\"$(release_source_ref || true)\""));
|
||||||
"unix installer must skip stale latest before downloading a prebuilt"
|
assert!(install.contains("checkout -q --detach FETCH_HEAD"));
|
||||||
);
|
|
||||||
|
|
||||||
let ps1 = include_str!("../install.ps1");
|
let ps1 = include_str!("../install.ps1");
|
||||||
assert!(ps1.contains("Latest-ReleaseIsStale"));
|
|
||||||
assert!(ps1.contains("Current-SourceVersion"));
|
assert!(ps1.contains("Current-SourceVersion"));
|
||||||
assert!(ps1.contains("latest release $latestVersion is older than source $current"));
|
assert!(ps1.contains("Source-ReleaseRef"));
|
||||||
assert!(ps1.contains("Version-Prerelease"));
|
assert!(ps1.contains("\"v$($current.TrimStart('v'))\""));
|
||||||
assert!(ps1.contains("Compare-Prerelease"));
|
assert!(ps1.contains("DOSH_BINARY_EXACT"));
|
||||||
assert!(
|
assert!(!ps1.contains("/releases/latest"));
|
||||||
ps1.contains("if (Latest-ReleaseIsStale)"),
|
assert!(ps1.contains("$script:ResolvedSourceRef = Source-ReleaseRef"));
|
||||||
"windows installer must skip stale latest before downloading a prebuilt"
|
assert!(ps1.contains("checkout -q --detach FETCH_HEAD"));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -198,15 +196,16 @@ fn windows_installer_reuses_persistent_source_update_cache() {
|
|||||||
assert!(ps1.contains("$full -eq $localAppData -or $full -eq $localAppDataDosh"));
|
assert!(ps1.contains("$full -eq $localAppData -or $full -eq $localAppDataDosh"));
|
||||||
assert!(ps1.contains("$sourceCache = Assert-SafeUpdateCache $UpdateCache"));
|
assert!(ps1.contains("$sourceCache = Assert-SafeUpdateCache $UpdateCache"));
|
||||||
assert!(ps1.contains(
|
assert!(ps1.contains(
|
||||||
"$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"--depth\", \"1\", \"origin\", \"main\")"
|
"$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"--depth\", \"1\", \"origin\", $sourceRef)"
|
||||||
));
|
));
|
||||||
assert!(ps1.contains("$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"-q\", \"--depth\", \"1\", \"origin\", \"main\")"));
|
assert!(ps1.contains("$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"-q\", \"--depth\", \"1\", \"origin\", $sourceRef)"));
|
||||||
assert!(ps1.contains("git @fetchArgs"));
|
assert!(ps1.contains("git @fetchArgs"));
|
||||||
assert!(ps1.contains("git -C $sourceCache checkout -q -B main FETCH_HEAD"));
|
assert!(ps1.contains("git -C $sourceCache checkout -q -B main FETCH_HEAD"));
|
||||||
|
assert!(ps1.contains("git -C $sourceCache checkout -q --detach FETCH_HEAD"));
|
||||||
assert!(ps1.contains(
|
assert!(ps1.contains(
|
||||||
"$cloneArgs = @(\"clone\", \"--depth\", \"1\", \"--branch\", \"main\", $Repo, $sourceCache)"
|
"$cloneArgs = @(\"clone\", \"--depth\", \"1\", \"--branch\", $sourceRef, $Repo, $sourceCache)"
|
||||||
));
|
));
|
||||||
assert!(ps1.contains("$cloneArgs = @(\"clone\", \"-q\", \"--depth\", \"1\", \"--branch\", \"main\", $Repo, $sourceCache)"));
|
assert!(ps1.contains("$cloneArgs = @(\"clone\", \"-q\", \"--depth\", \"1\", \"--branch\", $sourceRef, $Repo, $sourceCache)"));
|
||||||
assert!(ps1.contains("git @cloneArgs | Out-Null"));
|
assert!(ps1.contains("git @cloneArgs | Out-Null"));
|
||||||
assert!(
|
assert!(
|
||||||
!ps1.contains("git clone --depth 1 $Repo $tmp"),
|
!ps1.contains("git clone --depth 1 $Repo $tmp"),
|
||||||
@@ -218,8 +217,12 @@ fn windows_installer_reuses_persistent_source_update_cache() {
|
|||||||
fn windows_quiet_source_updates_silence_git_like_unix() {
|
fn windows_quiet_source_updates_silence_git_like_unix() {
|
||||||
let install = include_str!("../install.sh");
|
let install = include_str!("../install.sh");
|
||||||
let ps1 = include_str!("../install.ps1");
|
let ps1 = include_str!("../install.ps1");
|
||||||
assert!(install.contains("git -C \"$update_cache\" fetch -q --depth 1 origin main"));
|
assert!(install.contains("git -C \"$update_cache\" fetch -q --depth 1 origin \"$source_ref\""));
|
||||||
assert!(install.contains("git clone -q --depth 1 --branch main \"$repo\" \"$update_cache\""));
|
assert!(
|
||||||
|
install.contains(
|
||||||
|
"git clone -q --depth 1 --branch \"$source_ref\" \"$repo\" \"$update_cache\""
|
||||||
|
)
|
||||||
|
);
|
||||||
assert!(ps1.contains("if ($Quiet)"));
|
assert!(ps1.contains("if ($Quiet)"));
|
||||||
assert!(ps1.contains("\"fetch\", \"-q\", \"--depth\""));
|
assert!(ps1.contains("\"fetch\", \"-q\", \"--depth\""));
|
||||||
assert!(ps1.contains("\"clone\", \"-q\", \"--depth\""));
|
assert!(ps1.contains("\"clone\", \"-q\", \"--depth\""));
|
||||||
|
|||||||
Reference in New Issue
Block a user