browser4-cli 4.14.0-rc.5 → 4.14.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -21
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/scripts/README.md +86 -1
- package/scripts/install-browser4-cli.ps1 +174 -46
- package/scripts/install-browser4-cli.sh +98 -28
- package/scripts/reconcile-release-assets.sh +293 -0
- package/scripts/smoke-test-runtime-bundle.sh +18 -3
- package/scripts/tests/install-browser4-cli.tests.ps1 +150 -15
- package/scripts/tests/install-browser4-cli.tests.sh +157 -10
- package/scripts/tests/reconcile-release-assets.tests.sh +421 -0
- package/scripts/tests/wait-for-npm-version.tests.sh +219 -0
- package/scripts/tests/wait-for-oss-sync.tests.sh +382 -0
- package/scripts/wait-for-npm-version.sh +89 -0
- package/scripts/wait-for-oss-sync.sh +314 -0
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
|
|
51
51
|
.PARAMETER Force
|
|
52
52
|
Force reinstallation even if the binary is already installed at the target path.
|
|
53
|
-
Overrides -SkipIfInstalled
|
|
53
|
+
Overrides -SkipIfInstalled (the locked-file workaround in Set-BinaryFile is
|
|
54
|
+
always attempted, with or without this switch).
|
|
54
55
|
|
|
55
56
|
.PARAMETER SkipBackend
|
|
56
57
|
Skip installing/upgrading the Browser4 backend (runtime bundle).
|
|
@@ -115,6 +116,17 @@ param(
|
|
|
115
116
|
|
|
116
117
|
$ErrorActionPreference = "Stop"
|
|
117
118
|
|
|
119
|
+
# Windows PowerShell 5.1 on an older .NET defaults to TLS 1.0, which GitHub and
|
|
120
|
+
# the OSS mirror both refuse ("Could not create SSL/TLS secure channel"). Pin
|
|
121
|
+
# 1.2 when the runtime offers it; PowerShell 6+ already negotiates correctly.
|
|
122
|
+
if ($PSVersionTable.PSVersion.Major -lt 6) {
|
|
123
|
+
try {
|
|
124
|
+
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
|
125
|
+
} catch {
|
|
126
|
+
# Runtime without TLS 1.2 support -- keep its default
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
# ----------------------------------------------
|
|
119
131
|
# Script location -- find ourselves on disk
|
|
120
132
|
# ----------------------------------------------
|
|
@@ -280,16 +292,28 @@ function Test-ChinaLocale {
|
|
|
280
292
|
return $true
|
|
281
293
|
}
|
|
282
294
|
|
|
283
|
-
# 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
|
|
295
|
+
# 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+).
|
|
296
|
+
# Windows reports Windows ids ("China Standard Time"); the IANA ids only
|
|
297
|
+
# appear on Unix, so matching IANA alone made this branch dead on Windows.
|
|
284
298
|
try {
|
|
285
299
|
$tzId = [System.TimeZoneInfo]::Local.Id
|
|
286
|
-
if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
300
|
+
if ($tzId -eq 'China Standard Time' -or $tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
287
301
|
return $true
|
|
288
302
|
}
|
|
289
303
|
} catch {
|
|
290
304
|
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
291
305
|
}
|
|
292
306
|
|
|
307
|
+
# 3b -- current culture: a Chinese Windows install reports zh-CN even when
|
|
308
|
+
# no locale environment variables are set.
|
|
309
|
+
try {
|
|
310
|
+
if ([System.Globalization.CultureInfo]::CurrentCulture.Name -match '^zh-(CN|Hans)') {
|
|
311
|
+
return $true
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
# Culture lookup failed -- not fatal
|
|
315
|
+
}
|
|
316
|
+
|
|
293
317
|
# 4 -- /etc/timezone (PowerShell on Linux/macOS)
|
|
294
318
|
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
295
319
|
try {
|
|
@@ -345,6 +369,43 @@ function Get-DownloadUrls {
|
|
|
345
369
|
return $urls
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
<#
|
|
373
|
+
.SYNOPSIS
|
|
374
|
+
Check that a downloaded file really is a native executable.
|
|
375
|
+
Magic bytes are the cheapest integrity signal that needs no network and no
|
|
376
|
+
published checksum file: an HTML error page or a truncated body that happens
|
|
377
|
+
to be larger than the size threshold is rejected here.
|
|
378
|
+
#>
|
|
379
|
+
function Test-BinaryMagic {
|
|
380
|
+
param([string]$Path)
|
|
381
|
+
|
|
382
|
+
if (-not (Test-Path $Path)) { return $false }
|
|
383
|
+
# Read exactly four bytes through .NET: works on 5.1 and 7.x alike
|
|
384
|
+
# (Get-Content -Encoding Byte is gone in PowerShell 7).
|
|
385
|
+
$bytes = New-Object byte[] 4
|
|
386
|
+
$read = 0
|
|
387
|
+
try {
|
|
388
|
+
$stream = [System.IO.File]::OpenRead($Path)
|
|
389
|
+
try {
|
|
390
|
+
$read = $stream.Read($bytes, 0, 4)
|
|
391
|
+
} finally {
|
|
392
|
+
$stream.Dispose()
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
return $false
|
|
396
|
+
}
|
|
397
|
+
if ($read -lt 4) { return $false }
|
|
398
|
+
|
|
399
|
+
$magic = ($bytes | ForEach-Object { $_.ToString('x2') }) -join ''
|
|
400
|
+
|
|
401
|
+
switch -Regex ($magic) {
|
|
402
|
+
'^7f454c46' { return $true } # ELF (linux glibc / musl)
|
|
403
|
+
'^4d5a' { return $true } # PE / MZ (windows)
|
|
404
|
+
'^(feedface|feedfacf|cefaedfe|cffaedfe|cafebabe)$' { return $true } # Mach-O, incl. fat
|
|
405
|
+
default { return $false }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
348
409
|
function Invoke-Download {
|
|
349
410
|
param([string]$Url, [string]$OutFile, [string]$Label)
|
|
350
411
|
|
|
@@ -359,12 +420,16 @@ function Invoke-Download {
|
|
|
359
420
|
try {
|
|
360
421
|
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
361
422
|
|
|
362
|
-
# Use Invoke-WebRequest with progress bar
|
|
363
423
|
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
|
|
364
424
|
|
|
365
425
|
if (Test-Path $OutFile) {
|
|
366
426
|
$size = (Get-Item $OutFile).Length
|
|
367
427
|
if ($size -gt 102400) { # > 100 KB minimum
|
|
428
|
+
if (-not (Test-BinaryMagic -Path $OutFile)) {
|
|
429
|
+
Write-WarnMsg "Downloaded file is not a native executable (bad magic bytes) -- refusing it"
|
|
430
|
+
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
431
|
+
return $false
|
|
432
|
+
}
|
|
368
433
|
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
369
434
|
return $true
|
|
370
435
|
} else {
|
|
@@ -586,29 +651,39 @@ function Add-DirectoryToUserPath {
|
|
|
586
651
|
|
|
587
652
|
$dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
|
|
588
653
|
if (-not $dirResolved) { $dirResolved = $Dir }
|
|
654
|
+
# Trailing separators would defeat the duplicate check below.
|
|
655
|
+
$dirResolved = $dirResolved.TrimEnd('\', '/')
|
|
589
656
|
|
|
590
657
|
# Read current user PATH
|
|
591
658
|
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
|
592
659
|
$paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
|
|
593
660
|
|
|
594
|
-
# Check if already present
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
661
|
+
# Check if already present. Compare normalized, case-insensitively: Windows
|
|
662
|
+
# paths differ in case, a relative entry has to be resolved first, and a
|
|
663
|
+
# trailing separator is not a different directory.
|
|
664
|
+
$normalized = $paths | ForEach-Object {
|
|
665
|
+
$rp = Resolve-Path $_ -ErrorAction SilentlyContinue
|
|
666
|
+
$p = if ($rp) { $rp.Path } else { $_ }
|
|
667
|
+
$p.TrimEnd('\', '/')
|
|
668
|
+
}
|
|
669
|
+
if ($normalized | Where-Object { $_.Equals($dirResolved, [System.StringComparison]::OrdinalIgnoreCase) }) {
|
|
670
|
+
Write-Check "Already in user PATH: $dirResolved"
|
|
598
671
|
return
|
|
599
672
|
}
|
|
600
673
|
|
|
601
674
|
if ($DryRun) {
|
|
602
|
-
Write-Check "[DRY-RUN] Would add to user PATH: $
|
|
675
|
+
Write-Check "[DRY-RUN] Would add to user PATH: $dirResolved"
|
|
603
676
|
return
|
|
604
677
|
}
|
|
605
678
|
|
|
606
|
-
|
|
679
|
+
# Append the resolved absolute path: `-InstallDir .\b4` used to write a
|
|
680
|
+
# relative entry into the persistent user PATH.
|
|
681
|
+
$newPath = if ($currentUserPath) { "$currentUserPath;$dirResolved" } else { $dirResolved }
|
|
607
682
|
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
|
|
608
|
-
Write-Check "Added to user PATH: $
|
|
683
|
+
Write-Check "Added to user PATH: $dirResolved"
|
|
609
684
|
|
|
610
685
|
# Also update current session
|
|
611
|
-
$env:Path = "$env:Path;$
|
|
686
|
+
$env:Path = "$env:Path;$dirResolved"
|
|
612
687
|
}
|
|
613
688
|
|
|
614
689
|
# ----------------------------------------------
|
|
@@ -622,10 +697,16 @@ function Add-DirectoryToUserPath {
|
|
|
622
697
|
#>
|
|
623
698
|
function Get-BackendAction {
|
|
624
699
|
param([string]$StatusOutput)
|
|
625
|
-
|
|
700
|
+
# Only a status that positively reports an installed bundle means upgrade.
|
|
701
|
+
# An empty status (no CLI yet, or `status` itself failed) must not turn a
|
|
702
|
+
# fresh machine into an `upgrade`.
|
|
703
|
+
if ($StatusOutput -match 'Installed bundle:\s*not installed') {
|
|
626
704
|
return "install"
|
|
627
705
|
}
|
|
628
|
-
|
|
706
|
+
if ($StatusOutput -match 'Installed bundle:') {
|
|
707
|
+
return "upgrade"
|
|
708
|
+
}
|
|
709
|
+
return "install"
|
|
629
710
|
}
|
|
630
711
|
|
|
631
712
|
<#
|
|
@@ -635,28 +716,34 @@ function Get-BackendAction {
|
|
|
635
716
|
- no backend installed yet -> browser4-cli install
|
|
636
717
|
- backend already present -> browser4-cli upgrade (to the latest)
|
|
637
718
|
Passes -Version through as --tag so the backend matches the CLI version.
|
|
638
|
-
|
|
719
|
+
Returns $true when the backend is in place (or was skipped on request) and
|
|
720
|
+
$false when the step failed -- the caller turns that into the exit code, so a
|
|
721
|
+
broken backend is visible to scripts even though the CLI stays usable.
|
|
639
722
|
#>
|
|
640
723
|
function Install-Backend {
|
|
641
724
|
param([string]$CliPath, [string]$Tag)
|
|
642
725
|
|
|
643
726
|
if ($SkipBackend) {
|
|
644
727
|
Write-Step "Skipping backend install/upgrade (-SkipBackend)"
|
|
645
|
-
return
|
|
728
|
+
return $true
|
|
646
729
|
}
|
|
647
730
|
|
|
648
731
|
Write-Step "Checking for an existing Browser4 backend..."
|
|
649
732
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
# Binary missing or not runnable yet (e.g. --dry-run) -- fall through.
|
|
733
|
+
# -DryRun must not touch the installed CLI at all: ask what would happen
|
|
734
|
+
# instead of running `status`.
|
|
735
|
+
$action = "install"
|
|
736
|
+
if (-not $DryRun) {
|
|
655
737
|
$statusOutput = ""
|
|
738
|
+
try {
|
|
739
|
+
$statusOutput = (& $CliPath status 2>&1 | Out-String)
|
|
740
|
+
} catch {
|
|
741
|
+
# Binary missing or not runnable -- treat as "no backend yet".
|
|
742
|
+
$statusOutput = ""
|
|
743
|
+
}
|
|
744
|
+
$action = Get-BackendAction -StatusOutput $statusOutput
|
|
656
745
|
}
|
|
657
746
|
|
|
658
|
-
$action = Get-BackendAction -StatusOutput $statusOutput
|
|
659
|
-
|
|
660
747
|
$backendArgs = @($action)
|
|
661
748
|
if ($Tag) { $backendArgs += @('--tag', $Tag) }
|
|
662
749
|
|
|
@@ -668,9 +755,10 @@ function Install-Backend {
|
|
|
668
755
|
|
|
669
756
|
if ($DryRun) {
|
|
670
757
|
Write-Check "[DRY-RUN] Would run: $CliPath $($backendArgs -join ' ')"
|
|
671
|
-
return
|
|
758
|
+
return $true
|
|
672
759
|
}
|
|
673
760
|
|
|
761
|
+
$backendOk = $false
|
|
674
762
|
if ($Silent) {
|
|
675
763
|
$backendOutput = ""
|
|
676
764
|
try {
|
|
@@ -678,7 +766,8 @@ function Install-Backend {
|
|
|
678
766
|
} catch {
|
|
679
767
|
$backendOutput = $_.Exception.Message
|
|
680
768
|
}
|
|
681
|
-
|
|
769
|
+
$backendOk = ($LASTEXITCODE -eq 0)
|
|
770
|
+
if ($backendOk) {
|
|
682
771
|
Write-Check "Backend $action succeeded ($($backendArgs -join ' '))."
|
|
683
772
|
} else {
|
|
684
773
|
Write-WarnMsg "Backend $action failed ($($backendArgs -join ' ')). Retry with: browser4-cli $($backendArgs -join ' ')"
|
|
@@ -686,12 +775,15 @@ function Install-Backend {
|
|
|
686
775
|
}
|
|
687
776
|
} else {
|
|
688
777
|
& $CliPath @backendArgs
|
|
689
|
-
|
|
778
|
+
$backendOk = ($LASTEXITCODE -eq 0)
|
|
779
|
+
if ($backendOk) {
|
|
690
780
|
Write-Check "Backend $action succeeded ($($backendArgs -join ' '))."
|
|
691
781
|
} else {
|
|
692
782
|
Write-WarnMsg "Backend $action failed ($($backendArgs -join ' ')). Retry with: browser4-cli $($backendArgs -join ' ')"
|
|
693
783
|
}
|
|
694
784
|
}
|
|
785
|
+
|
|
786
|
+
return $backendOk
|
|
695
787
|
}
|
|
696
788
|
|
|
697
789
|
# ----------------------------------------------
|
|
@@ -704,6 +796,19 @@ function Main {
|
|
|
704
796
|
Write-Summary "==========================================" -Color Cyan
|
|
705
797
|
Write-Summary ""
|
|
706
798
|
|
|
799
|
+
# Validate -Version before it is interpolated into a download URL: a value
|
|
800
|
+
# like '../../evil' used to produce a valid-looking URL and also reached
|
|
801
|
+
# `--tag`. A bare semver gets the 'v' prefix, as in the Unix installer.
|
|
802
|
+
if ($Version) {
|
|
803
|
+
if ($Version -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') {
|
|
804
|
+
throw "-Version must be a release tag such as v4.13.18 (got '$Version')"
|
|
805
|
+
}
|
|
806
|
+
if ($Version -match '^[0-9]+\.[0-9]+\.[0-9]+') {
|
|
807
|
+
Write-Step "Using release tag 'v$Version' (release tags are prefixed with 'v')"
|
|
808
|
+
$Version = "v$Version"
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
707
812
|
# Auto-detect China mainland locale when no explicit source is given
|
|
708
813
|
if (-not $Source) {
|
|
709
814
|
$script:ChinaDetected = Test-ChinaLocale
|
|
@@ -723,7 +828,7 @@ function Main {
|
|
|
723
828
|
Write-Step "Script dir: $ScriptDir"
|
|
724
829
|
Write-Step "Platform key: $platformKey"
|
|
725
830
|
Write-Step "Binary name: $binaryName"
|
|
726
|
-
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
831
|
+
Write-Step "Default install: $(if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir })"
|
|
727
832
|
Write-Step "China locale: $script:ChinaDetected"
|
|
728
833
|
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
729
834
|
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
@@ -810,18 +915,20 @@ function Main {
|
|
|
810
915
|
}
|
|
811
916
|
|
|
812
917
|
$downloaded = $false
|
|
918
|
+
# The temp file is always cleaned up, including on -DryRun (where the
|
|
919
|
+
# download never consumes it) and on the failure path.
|
|
813
920
|
$tempFile = [System.IO.Path]::GetTempFileName()
|
|
814
921
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
$
|
|
818
|
-
|
|
922
|
+
try {
|
|
923
|
+
foreach ($entry in $urls) {
|
|
924
|
+
if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
|
|
925
|
+
$downloaded = $true
|
|
926
|
+
break
|
|
927
|
+
}
|
|
819
928
|
}
|
|
820
|
-
}
|
|
821
929
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
throw @"
|
|
930
|
+
if (-not $downloaded) {
|
|
931
|
+
throw @"
|
|
825
932
|
Could not download browser4-cli binary.
|
|
826
933
|
|
|
827
934
|
Tried:
|
|
@@ -833,13 +940,16 @@ Please check:
|
|
|
833
940
|
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
834
941
|
- If you have a local copy, place it alongside this script and re-run
|
|
835
942
|
"@
|
|
836
|
-
|
|
943
|
+
}
|
|
837
944
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
945
|
+
# Move from temp to install dir
|
|
946
|
+
if (-not $DryRun) {
|
|
947
|
+
Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
|
|
948
|
+
}
|
|
949
|
+
Write-Check "Installed: $binaryPath"
|
|
950
|
+
} finally {
|
|
951
|
+
if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue }
|
|
841
952
|
}
|
|
842
|
-
Write-Check "Installed: $binaryPath"
|
|
843
953
|
}
|
|
844
954
|
|
|
845
955
|
# On Unix, ensure executable bit
|
|
@@ -875,16 +985,28 @@ Please check:
|
|
|
875
985
|
}
|
|
876
986
|
}
|
|
877
987
|
|
|
878
|
-
# Verify
|
|
988
|
+
# Verify. A binary that cannot report its version is a failed install --
|
|
989
|
+
# report it red and exit non-zero instead of printing a green success line.
|
|
879
990
|
Write-Summary ""
|
|
991
|
+
$script:ExitCode = 0
|
|
880
992
|
if (-not $DryRun) {
|
|
993
|
+
$versionOutput = ""
|
|
994
|
+
$versionOk = $false
|
|
881
995
|
try {
|
|
882
|
-
$versionOutput = & $binaryPath --version 2>&1
|
|
996
|
+
$versionOutput = (& $binaryPath --version 2>&1 | Out-String).Trim()
|
|
997
|
+
$versionOk = ($LASTEXITCODE -eq 0) -and ($versionOutput -match '\d')
|
|
998
|
+
} catch {
|
|
999
|
+
$versionOk = $false
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if ($versionOk) {
|
|
883
1003
|
Write-Summary "[v] browser4-cli installed successfully" -Color Green
|
|
884
1004
|
Write-Summary " Version: $versionOutput"
|
|
885
|
-
}
|
|
886
|
-
Write-Summary "[
|
|
887
|
-
Write-
|
|
1005
|
+
} else {
|
|
1006
|
+
Write-Summary "[x] Installed $binaryPath but it cannot run:" -Color Red
|
|
1007
|
+
Write-Summary " $versionOutput"
|
|
1008
|
+
Write-WarnMsg "The binary may be corrupt or built for a different platform. Delete it and re-run the installer."
|
|
1009
|
+
$script:ExitCode = 1
|
|
888
1010
|
}
|
|
889
1011
|
} else {
|
|
890
1012
|
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
@@ -893,8 +1015,12 @@ Please check:
|
|
|
893
1015
|
# Install / upgrade the Browser4 backend (runtime bundle) using the CLI
|
|
894
1016
|
# binary we just installed. Fresh machines get `browser4-cli install`;
|
|
895
1017
|
# machines that already have a backend get `browser4-cli upgrade`.
|
|
1018
|
+
# A backend failure keeps the CLI usable but must be visible to scripts
|
|
1019
|
+
# and CI, so it is reflected in the exit code.
|
|
896
1020
|
Write-Summary ""
|
|
897
|
-
Install-Backend -CliPath $binaryPath -Tag $Version
|
|
1021
|
+
if ((Install-Backend -CliPath $binaryPath -Tag $Version) -eq $false) {
|
|
1022
|
+
$script:ExitCode = 1
|
|
1023
|
+
}
|
|
898
1024
|
|
|
899
1025
|
Write-Summary ""
|
|
900
1026
|
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
@@ -903,6 +1029,8 @@ Please check:
|
|
|
903
1029
|
Write-Summary "If the command isn't found, restart your terminal or run:"
|
|
904
1030
|
Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
|
|
905
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
exit $script:ExitCode
|
|
906
1034
|
}
|
|
907
1035
|
|
|
908
1036
|
Main
|
|
@@ -55,11 +55,41 @@ SCRIPT_DIR=""
|
|
|
55
55
|
# ----------------------------------------------
|
|
56
56
|
|
|
57
57
|
say() { if [[ "$SILENT" != true ]]; then echo -e "$*"; fi; }
|
|
58
|
+
# Informational note for helpers whose stdout is captured by the caller --
|
|
59
|
+
# anything these functions print on stdout ends up inside the captured value.
|
|
60
|
+
note() { if [[ "$SILENT" != true ]]; then echo -e " [i] $*" >&2; fi; }
|
|
58
61
|
step() { say " -> $*"; }
|
|
59
62
|
ok() { say " [v] $*"; }
|
|
60
63
|
warn() { say " [!] $*" >&2; }
|
|
61
64
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
62
65
|
|
|
66
|
+
# Validate the value that must follow an option. A missing value and a value
|
|
67
|
+
# that is really the next option are both rejected: `--version --dry-run` used
|
|
68
|
+
# to install the literal tag "--dry-run" (and `--install-dir --silent` created
|
|
69
|
+
# a directory called "--silent"), which fails much later with a confusing 404.
|
|
70
|
+
# Runs in the current shell so `die` really stops the installer -- a `die`
|
|
71
|
+
# inside `$( ... )` only kills the subshell and the install would continue.
|
|
72
|
+
validate_value() {
|
|
73
|
+
local flag="$1" value="${2:-}"
|
|
74
|
+
if [[ -z "$value" ]]; then
|
|
75
|
+
die "$flag requires a value"
|
|
76
|
+
fi
|
|
77
|
+
if [[ "$value" == -* ]]; then
|
|
78
|
+
die "$flag requires a value (got '$value', which looks like another option)"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Release tags on GitHub are prefixed with "v" (v4.13.18). Accepting a bare
|
|
83
|
+
# "4.13.18" and fixing it up front is friendlier than a 404 from the CDN.
|
|
84
|
+
normalize_tag() {
|
|
85
|
+
local tag="$1"
|
|
86
|
+
if [[ "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
|
87
|
+
note "Using release tag 'v$tag' (release tags are prefixed with 'v')"
|
|
88
|
+
tag="v$tag"
|
|
89
|
+
fi
|
|
90
|
+
printf '%s' "$tag"
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
color_cyan='\033[0;36m'
|
|
64
94
|
color_green='\033[0;32m'
|
|
65
95
|
color_yellow='\033[0;33m'
|
|
@@ -157,13 +187,12 @@ EOF
|
|
|
157
187
|
while [[ $# -gt 0 ]]; do
|
|
158
188
|
case "$1" in
|
|
159
189
|
--version|-v)
|
|
160
|
-
shift;
|
|
161
|
-
VERSION="$1"; shift ;;
|
|
190
|
+
shift; validate_value "--version" "${1:-}"; VERSION=$(normalize_tag "$1"); shift ;;
|
|
162
191
|
--install-dir|-d)
|
|
163
|
-
shift;
|
|
164
|
-
INSTALL_DIR="$1"; shift ;;
|
|
192
|
+
shift; validate_value "--install-dir" "${1:-}"; INSTALL_DIR="$1"; shift ;;
|
|
165
193
|
--source)
|
|
166
|
-
shift
|
|
194
|
+
shift
|
|
195
|
+
validate_value "--source" "${1:-}"
|
|
167
196
|
if [[ "$1" != "github" && "$1" != "oss" ]]; then
|
|
168
197
|
die "--source must be 'github' or 'oss'"
|
|
169
198
|
fi
|
|
@@ -316,7 +345,7 @@ get_default_install_dir() {
|
|
|
316
345
|
|
|
317
346
|
check_commands() {
|
|
318
347
|
local missing=()
|
|
319
|
-
for cmd in curl mktemp stat awk grep ln mkdir chmod; do
|
|
348
|
+
for cmd in curl mktemp stat awk grep ln mkdir chmod od cp mv tail cat; do
|
|
320
349
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
321
350
|
missing+=("$cmd")
|
|
322
351
|
fi
|
|
@@ -326,6 +355,21 @@ check_commands() {
|
|
|
326
355
|
fi
|
|
327
356
|
}
|
|
328
357
|
|
|
358
|
+
# A downloaded file must really be a native executable, not an HTML error page
|
|
359
|
+
# or a wrong-architecture body that happens to be large. Magic bytes are the
|
|
360
|
+
# cheapest check that needs no network and no published checksum file.
|
|
361
|
+
verify_binary_magic() {
|
|
362
|
+
local path="$1"
|
|
363
|
+
local magic
|
|
364
|
+
magic=$(od -An -tx1 -N4 "$path" 2>/dev/null | tr -d ' \n')
|
|
365
|
+
case "$magic" in
|
|
366
|
+
7f454c46) return 0 ;; # ELF (linux glibc/musl)
|
|
367
|
+
4d5a*) return 0 ;; # PE / MZ (windows)
|
|
368
|
+
feedface|feedfacf|cefaedfe|cffaedfe|cafebabe) return 0 ;; # Mach-O, incl. fat
|
|
369
|
+
*) return 1 ;;
|
|
370
|
+
esac
|
|
371
|
+
}
|
|
372
|
+
|
|
329
373
|
get_download_urls() {
|
|
330
374
|
local binary_name="$1"
|
|
331
375
|
local version_tag="$2"
|
|
@@ -374,8 +418,15 @@ download_file() {
|
|
|
374
418
|
curl_stderr=$(mktemp)
|
|
375
419
|
local http_code curl_exit
|
|
376
420
|
|
|
421
|
+
# Only GitHub needs (and only GitHub may receive) the token: the same header
|
|
422
|
+
# must never be sent to the Aliyun OSS mirror.
|
|
423
|
+
local -a auth_args=()
|
|
424
|
+
if [[ -n "${GITHUB_TOKEN:-}" ]] && [[ "$url" == https://github.com/* ]]; then
|
|
425
|
+
auth_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
|
|
426
|
+
fi
|
|
427
|
+
|
|
377
428
|
http_code=$(curl -sSfL -w "%{http_code}" -o "$dest" \
|
|
378
|
-
${
|
|
429
|
+
${auth_args[@]+"${auth_args[@]}"} \
|
|
379
430
|
"$url" 2>"$curl_stderr")
|
|
380
431
|
curl_exit=$?
|
|
381
432
|
|
|
@@ -398,6 +449,11 @@ download_file() {
|
|
|
398
449
|
|
|
399
450
|
# Sanity check: binary must be > 100 KB
|
|
400
451
|
if [[ "$size" -gt 102400 ]]; then
|
|
452
|
+
if ! verify_binary_magic "$dest"; then
|
|
453
|
+
warn "Downloaded file is not a native executable (bad magic bytes) -- refusing it"
|
|
454
|
+
rm -f "$dest"
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
401
457
|
local size_mb
|
|
402
458
|
size_mb=$(awk "BEGIN { printf \"%.1f\", $size / 1048576 }")
|
|
403
459
|
ok "Downloaded ${size_mb} MB"
|
|
@@ -439,7 +495,11 @@ add_to_shell_rc() {
|
|
|
439
495
|
rc_file="$HOME/.profile"
|
|
440
496
|
fi
|
|
441
497
|
|
|
442
|
-
|
|
498
|
+
local path_line="export PATH=\"$dir:\$PATH\""
|
|
499
|
+
|
|
500
|
+
# Idempotency: only PATH assignment lines count, so an unrelated mention of the
|
|
501
|
+
# directory (a comment, another tool's variable) does not suppress the entry.
|
|
502
|
+
if grep -E '^[[:space:]]*(export[[:space:]]+)?PATH=' "$rc_file" 2>/dev/null | grep -qF "$dir"; then
|
|
443
503
|
ok "PATH entry already in $rc_file"
|
|
444
504
|
return
|
|
445
505
|
fi
|
|
@@ -454,23 +514,25 @@ add_to_shell_rc() {
|
|
|
454
514
|
fi
|
|
455
515
|
fi
|
|
456
516
|
|
|
457
|
-
#
|
|
517
|
+
# Serialize concurrent installs when the platform has flock (Linux). macOS
|
|
518
|
+
# ships without it, and waiting for a lock that can never be taken only made
|
|
519
|
+
# every install pause and print a bogus warning.
|
|
458
520
|
local lock_file="${rc_file}.browser4-install.lock"
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
521
|
+
if command -v flock >/dev/null 2>&1; then
|
|
522
|
+
local waited=0
|
|
523
|
+
while ! flock -n 9 2>/dev/null; do
|
|
524
|
+
if [[ $waited -ge 20 ]]; then
|
|
525
|
+
warn "Could not acquire lock on ${rc_file} after 10s; appending anyway"
|
|
526
|
+
break
|
|
527
|
+
fi
|
|
528
|
+
sleep 0.5
|
|
529
|
+
waited=$((waited + 1))
|
|
530
|
+
done 9>"$lock_file"
|
|
531
|
+
fi
|
|
470
532
|
|
|
471
533
|
{
|
|
472
534
|
echo "# browser4-cli"
|
|
473
|
-
echo "
|
|
535
|
+
echo "$path_line"
|
|
474
536
|
} >> "$rc_file"
|
|
475
537
|
|
|
476
538
|
# Clean up (flock auto-releases when fd 9 is closed)
|
|
@@ -511,7 +573,7 @@ create_symlinks() {
|
|
|
511
573
|
local b4_exists=false
|
|
512
574
|
local b4_is_ours=false
|
|
513
575
|
|
|
514
|
-
# Check in install dir first
|
|
576
|
+
# Check in install dir first - anything here is ours
|
|
515
577
|
if [[ -e "$short_path" ]] || [[ -L "$short_path" ]]; then
|
|
516
578
|
b4_exists=true
|
|
517
579
|
b4_is_ours=true
|
|
@@ -620,7 +682,6 @@ install_backend() {
|
|
|
620
682
|
# ----------------------------------------------
|
|
621
683
|
|
|
622
684
|
main() {
|
|
623
|
-
check_commands
|
|
624
685
|
header
|
|
625
686
|
|
|
626
687
|
# Locate ourselves on disk (only works when run as a file, not piped)
|
|
@@ -663,9 +724,9 @@ main() {
|
|
|
663
724
|
step "Local binary: not found alongside script"
|
|
664
725
|
fi
|
|
665
726
|
|
|
666
|
-
# Check for already-installed binary
|
|
727
|
+
# Check for already-installed binary (honour an explicit --install-dir)
|
|
667
728
|
local default_dir existing_path
|
|
668
|
-
default_dir
|
|
729
|
+
default_dir="${INSTALL_DIR:-$(get_default_install_dir)}"
|
|
669
730
|
existing_path="${default_dir}/${binary_name}"
|
|
670
731
|
if [[ -f "$existing_path" ]]; then
|
|
671
732
|
step "Already installed: $existing_path"
|
|
@@ -690,6 +751,10 @@ main() {
|
|
|
690
751
|
step "Platform: $platform_key"
|
|
691
752
|
step "Binary: $binary_name"
|
|
692
753
|
|
|
754
|
+
# Everything below downloads, unpacks or writes: check the tooling now, so
|
|
755
|
+
# `--locate` still works on a box that has none of it.
|
|
756
|
+
check_commands
|
|
757
|
+
|
|
693
758
|
# Install directory
|
|
694
759
|
if [[ -z "$INSTALL_DIR" ]]; then
|
|
695
760
|
INSTALL_DIR=$(get_default_install_dir)
|
|
@@ -699,10 +764,15 @@ main() {
|
|
|
699
764
|
|
|
700
765
|
# Ensure install directory exists
|
|
701
766
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
702
|
-
if [[ "$
|
|
703
|
-
|
|
767
|
+
if [[ -e "$INSTALL_DIR" ]]; then
|
|
768
|
+
die "Install path exists but is not a directory: $INSTALL_DIR"
|
|
769
|
+
fi
|
|
770
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
771
|
+
step "[DRY-RUN] Would create directory: $INSTALL_DIR"
|
|
772
|
+
else
|
|
773
|
+
mkdir -p "$INSTALL_DIR" || die "Could not create install directory: $INSTALL_DIR"
|
|
774
|
+
step "Created directory: $INSTALL_DIR"
|
|
704
775
|
fi
|
|
705
|
-
step "Created directory: $INSTALL_DIR"
|
|
706
776
|
fi
|
|
707
777
|
|
|
708
778
|
local binary_path="${INSTALL_DIR}/${binary_name}"
|