sciencemate-pro/install.ps1
2026-09-23 00:36:16 +08:00

163 lines
8.6 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ScienceMate —— 内网安装(Windows,不经浏览器,不弹 SmartScreen)
#
# ⚠️ 这个文件必须存成 **UTF-8 with BOM**。Windows 自带的 PowerShell 5.1 读
# 无 BOM 的 .ps1 时按机器的 ANSI 代码页解码 —— 里面的中文字符串会碎成乱码,
# 而乱码里的字节可能正好是引号,于是整个脚本变成语法错误(2026-09-21 实测:
# `Unexpected token` + `Missing closing ')'`,报错位置指向一行好端端的代码)。
# BOM 一摆,5.1 就按 UTF-8 读。和 csc 要 `/codepage:65001` 是同一件事。
#
# irm <发布目录>/install.ps1 | iex
#
# ## 为什么这条路不会弹「Windows 已保护你的电脑」
#
# SmartScreen 拦的不是「没签名的程序」,是「带着网络来源标记(Mark-of-the-Web)的
# 程序」。那个标记是**浏览器、邮件客户端、解压工具**在保存文件时,通过 Windows 的
# Attachment Manager 写进文件的 `Zone.Identifier` 备用数据流里的。`Invoke-WebRequest`
# 不写 —— 2026-09-21 在 Windows 11 上实测:iwr 和 WebClient 下来的文件都只有
# `:$DATA` 一个流,没有 `Zone.Identifier`。
#
# 所以经这条路装的应用,系统根本不会去问它签没签名。这不是绕过,是 Windows 自己
# 划的线:**文件从哪来,由谁负责**。内网发布目录由我们自己负责,和从共享盘拷过来
# 是一回事。
#
# 这一点很要紧,因为「买张证书就不弹了」在 2024 年 8 月之后**不再成立** ——
# 微软那时把 EV 证书的 SmartScreen 特权从信任根里撤了,现在所有代码签名证书一视同仁,
# 一律靠下载量慢慢攒声誉。一个内部工具攒不到那个量。详见 docs/WINDOWS_CODE_SIGNING.md。
#
# 脚本最后还是会 `Unblock-File` 一次:万一有人先用浏览器下了 Setup.exe、再手动跑
# 这个脚本,那一份是带标记的。
#
# ## 它做什么
#
# 下 SHA256SUMS → 从里面认出安装器叫什么 → 下它 → 核 SHA-256 → 去标记 → 运行。
# 每一步失败就停,不留半个安装。
#
# 环境变量(`irm | iex` 传不了参数,所以配置走环境变量):
# AFS_RELEASE_URL 发布目录(打包器已经烧了默认值)
# AFS_AUTH 私有 Forgejo 的 "user:token"(只读即可)
# AFS_EDITION personal(默认)/ pro
# AFS_INSTALL_DIR 装到哪(默认让向导问)
# AFS_SILENT =1 无人值守:不弹向导、装完不启动应用,但快捷方式照建
# AFS_DRY_RUN =1 只说要做什么,不做
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0
$ProgressPreference = 'SilentlyContinue'
# 下面这一行由打包器整行替换(占位符只许出现在这里 —— install.sh 第一版把占位符
# 也写进了判断里,替换后要么语法错、要么把正确的地址当成"没填"拒掉)。
$DefaultUrl = 'https://desktop-9el2944.taile9f15e.ts.net/wangd/sciencemate-pro/releases/download/v0.5.3'
$Base = if ($env:AFS_RELEASE_URL) { $env:AFS_RELEASE_URL } else { $DefaultUrl }
$Auth = $env:AFS_AUTH
$Edition = if ($env:AFS_EDITION) { $env:AFS_EDITION } else { 'personal' }
$Dry = [bool]$env:AFS_DRY_RUN
function Die($message) { Write-Host "✗ $message" -ForegroundColor Red; exit 1 }
function Say($message) { Write-Host $message }
if (-not $Base -or $Base -eq ('__RELEASE' + '_URL__')) {
Die "没有发布目录地址。打包时传 --release-url,或运行前 `$env:AFS_RELEASE_URL='http://<内网主机>/afs'"
}
if ([Environment]::OSVersion.Platform -ne 'Win32NT') { Die "这个脚本只装 Windows;macOS 用 install.sh" }
if (-not [Environment]::Is64BitOperatingSystem) { Die "这个包是 x64 的,这台不是 64 位 Windows" }
$Base = $Base.TrimEnd('/')
$Headers = @{}
if ($Auth) {
$Headers['Authorization'] = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Auth))
}
# 下载用 WebClient,**不用 Invoke-WebRequest**。
#
# 安装包是 350 MB 上下,而 PowerShell 5.1(Windows 自带的那个)的 `iwr -OutFile`
# 处理大文件慢得不像话:2026-09-21 实测,从**本机回环** HTTP 拉这个包,一分多钟
# 才下到一半,还一直占着 CPU —— 真用户从内网拉会等到以为脚本挂了。
# `WebClient.DownloadFile` 拉同一个文件是秒级的。
#
# 它一样不打网络来源标记(同一天实测:iwr 和 WebClient 下来的文件都只有 `:$DATA`
# 一个流),所以换它不影响这条路「不弹 SmartScreen」的前提。
function Fetch($relative, $destination) {
$url = "$Base/$relative"
if ($Dry) { Say " 下载 $url → $destination"; return }
$client = New-Object System.Net.WebClient
try {
foreach ($key in $Headers.Keys) { $client.Headers.Add($key, $Headers[$key]) }
$client.DownloadFile($url, $destination)
} finally { $client.Dispose() }
}
$work = Join-Path $env:TEMP ("afs-install-" + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work -Force | Out-Null
try {
Say "① 取校验和清单(它同时告诉我们安装器叫什么)"
$sumsPath = Join-Path $work 'SHA256SUMS'
Fetch 'SHA256SUMS' $sumsPath
if ($Dry) { $lines = @() } else { $lines = Get-Content $sumsPath }
# 安装器的文件名**从清单里认**,不烧进脚本:发布目录里有什么由清单说了算,
# 两处各写一份的话,改了名字的那一天脚本会去下一个不存在的文件。
#
# 不按 AFS_EDITION 去滤:一个发布目录只发一种发行(publish_release.py 有一道闸
# 守着这条),所以正常情况下清单里就只有一个 `*Setup.exe`,照用即可。按 edition
# 滤反而会在专业版目录里滤出 0 个 —— 那儿只有 `ScienceMate-Pro-Setup.exe`,而
# AFS_EDITION 默认是 personal。只有真出现两个时,AFS_EDITION 才有话要说。
$entries = @()
foreach ($line in $lines) {
$parts = $line -split '\s+', 2
if ($parts.Count -lt 2) { continue }
$name = $parts[1].Trim()
if ($name -notlike '*Setup.exe') { continue }
$entries += [pscustomobject]@{ Sha = $parts[0].Trim().ToLower(); Name = $name }
}
if ($entries.Count -gt 1) {
$wantPro = ($Edition -eq 'pro')
$entries = @($entries | Where-Object { ($_.Name -like '*-Pro-Setup.exe') -eq $wantPro })
}
if (-not $Dry -and $entries.Count -ne 1) {
Die ("清单里能对上「$Edition 版安装器」的有 " + $entries.Count + " 个,应当恰好 1 个。" +
"清单:" + (($lines | Where-Object { $_ -like '*Setup.exe' }) -join '; '))
}
$setupName = if ($Dry) { 'ScienceMate-Setup.exe' } else { $entries[0].Name }
$setupPath = Join-Path $work $setupName
Say "② 下载 $setupName(不经浏览器,文件不会带上网络来源标记)"
Fetch $setupName $setupPath
Say "③ 核对 SHA-256"
if (-not $Dry) {
$actual = (Get-FileHash -Path $setupPath -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $entries[0].Sha) {
Die "校验和对不上:清单说 $($entries[0].Sha),下下来的是 $actual。没装,删掉了。"
}
Say " $actual ✓"
# 保险:万一这一份是别处(浏览器)来的,把来源标记去掉。
Unblock-File -Path $setupPath
}
Say "④ 运行安装程序"
$arguments = @()
# 静默=无人值守。装完**不启动应用**:没人在屏幕前,弹出来的窗口没人看;而且
# 这条路多半跑在别的脚本里,起一个长命进程会让调用方的管道一直等着它 ——
# 2026-09-21 实测就是这样挂住的,调用方以为安装卡死了,其实早装完了。
# 快捷方式照建,人回到机器前从桌面或开始菜单打开。
if ($env:AFS_SILENT) {
$arguments += '--no-launch'
$arguments += '--desktop-shortcut'; $arguments += '--start-menu'; $arguments += '--register-uninstall'
}
if ($env:AFS_INSTALL_DIR) { $arguments += '--install-dir'; $arguments += $env:AFS_INSTALL_DIR }
if ($Dry) { Say (" $setupPath " + ($arguments -join ' ')); exit 0 }
$proc = if ($arguments.Count -gt 0) {
Start-Process -FilePath $setupPath -ArgumentList $arguments -Wait -PassThru
} else {
Start-Process -FilePath $setupPath -Wait -PassThru
}
if ($proc.ExitCode -ne 0) { Die "安装程序退出码 $($proc.ExitCode) —— 没装成。" }
if ($env:AFS_SILENT) { Say "✓ 装好了。从桌面或开始菜单打开 ScienceMate。" }
else { Say "✓ 装好了。" }
}
finally {
# 安装器已经把自己要的东西解到安装目录了,临时副本没有留下的理由。
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
}