360 lines
12 KiB
PowerShell
360 lines
12 KiB
PowerShell
#Requires -Version 5.1
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
pptopic 一键安装脚本
|
||
|
||
.DESCRIPTION
|
||
自动完成以下安装步骤:
|
||
1. 安装 uv(Python 包管理器)
|
||
2. 安装 Python 3.13
|
||
3. 安装 pngquant(可选但推荐)
|
||
4. 安装 pptopic
|
||
5. 验证安装结果
|
||
|
||
本脚本会自动处理 PowerShell 执行策略和当前会话 PATH 刷新,
|
||
安装完成后无需手动重启 PowerShell。
|
||
|
||
.PARAMETER SkipPngquant
|
||
跳过 pngquant 安装。
|
||
|
||
.PARAMETER Force
|
||
强制重新安装 uv 和 pngquant。
|
||
|
||
.PARAMETER SkipPptopicInstall
|
||
只安装环境,不安装 pptopic(调试用)。
|
||
|
||
.EXAMPLE
|
||
.\setup.ps1
|
||
|
||
.EXAMPLE
|
||
.\setup.ps1 -SkipPngquant
|
||
#>
|
||
|
||
param(
|
||
[switch]$SkipPngquant,
|
||
[switch]$Force,
|
||
[switch]$SkipPptopicInstall
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
$InformationPreference = "Continue"
|
||
|
||
$ProjectRoot = $PSScriptRoot
|
||
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
|
||
$ProjectRoot = (Get-Location).Path
|
||
}
|
||
|
||
# ============================================================
|
||
# 工具函数
|
||
# ============================================================
|
||
|
||
function Test-CommandExists {
|
||
param([string]$Name)
|
||
try {
|
||
$cmd = Get-Command $Name -ErrorAction Stop
|
||
return $cmd.Source
|
||
} catch {
|
||
return $null
|
||
}
|
||
}
|
||
|
||
function Add-ToCurrentPath {
|
||
param([string]$LiteralPath)
|
||
|
||
if ([string]::IsNullOrWhiteSpace($LiteralPath)) {
|
||
return
|
||
}
|
||
|
||
$currentPaths = $env:Path -split ';' | ForEach-Object { $_.TrimEnd('\') }
|
||
$normalizedPath = $LiteralPath.TrimEnd('\')
|
||
|
||
if ($normalizedPath -in $currentPaths) {
|
||
return
|
||
}
|
||
|
||
if (-not (Test-Path $LiteralPath)) {
|
||
return
|
||
}
|
||
|
||
$env:Path = "$LiteralPath;$env:Path"
|
||
Write-Information "已临时将 $LiteralPath 加入当前会话 PATH"
|
||
}
|
||
|
||
function Get-UvInstallDir {
|
||
# uv-installer.ps1 的安装位置优先级
|
||
$candidates = @()
|
||
if ($env:XDG_BIN_HOME) {
|
||
$candidates += $env:XDG_BIN_HOME
|
||
}
|
||
if ($env:XDG_DATA_HOME) {
|
||
$candidates += (Join-Path $env:XDG_DATA_HOME "../bin")
|
||
}
|
||
$candidates += (Join-Path $HOME ".local\bin")
|
||
|
||
foreach ($candidate in $candidates) {
|
||
$resolved = $null
|
||
try {
|
||
$resolved = (Resolve-Path $candidate -ErrorAction SilentlyContinue).Path
|
||
} catch {
|
||
$resolved = $candidate
|
||
}
|
||
if ($resolved -and (Test-Path $resolved)) {
|
||
return $resolved
|
||
}
|
||
}
|
||
|
||
# 默认返回最后一个候选
|
||
return Join-Path $HOME ".local\bin"
|
||
}
|
||
|
||
function Invoke-NativeCommand {
|
||
# 安全地调用原生程序(uv/pngquant 等)。
|
||
# 这些程序的进度/提示信息常写到 stderr(例如 "Python 3.13 is already installed"),
|
||
# 而本脚本 $ErrorActionPreference="Stop" 会把 stderr 当作终止错误抛出,导致重跑误失败。
|
||
# 此函数临时关闭 Stop,合并 stdout/stderr,并按退出码判断成败。
|
||
param(
|
||
[Parameter(Mandatory)][string]$LiteralCommand,
|
||
[string]$FailureMessage
|
||
)
|
||
|
||
$prevEAP = $ErrorActionPreference
|
||
$ErrorActionPreference = 'Continue'
|
||
try {
|
||
$output = Invoke-Expression "$LiteralCommand 2>&1"
|
||
} finally {
|
||
$ErrorActionPreference = $prevEAP
|
||
}
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
$msg = if ($FailureMessage) { $FailureMessage } else { "命令失败" }
|
||
throw "$msg(退出码 $LASTEXITCODE):$($output -join "`n")"
|
||
}
|
||
|
||
return $output
|
||
}
|
||
|
||
function Test-PowerPointInstalled {
|
||
# 通过注册表 ProgID 检测,不启动 PowerPoint 进程
|
||
try {
|
||
$null = Get-Item "Registry::HKEY_CLASSES_ROOT\PowerPoint.Application" -ErrorAction Stop
|
||
return $true
|
||
} catch {
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Get-PngquantInstallDir {
|
||
# pngquant 可能的安装位置:默认 %APPDATA%\pngquant,或 scoop 的 shims 目录
|
||
$candidates = @("$env:APPDATA\pngquant")
|
||
if ($env:SCOOP) {
|
||
$candidates += (Join-Path $env:SCOOP "shims")
|
||
}
|
||
$candidates += (Join-Path $HOME "scoop\shims")
|
||
|
||
foreach ($candidate in $candidates) {
|
||
if (Test-Path $candidate) {
|
||
return $candidate
|
||
}
|
||
}
|
||
|
||
# 找不到就返回默认(即便不存在,Add-ToCurrentPath 内部会跳过)
|
||
return "$env:APPDATA\pngquant"
|
||
}
|
||
|
||
function Invoke-Step {
|
||
param(
|
||
[Parameter(Mandatory)][string]$Title,
|
||
[Parameter(Mandatory)][scriptblock]$Action
|
||
)
|
||
|
||
Write-Information ""
|
||
Write-Information "========================================"
|
||
Write-Information " $Title"
|
||
Write-Information "========================================"
|
||
|
||
try {
|
||
& $Action
|
||
} catch {
|
||
Write-Information ""
|
||
Write-Information "失败:$_"
|
||
throw
|
||
}
|
||
}
|
||
|
||
# ============================================================
|
||
# 主流程
|
||
# ============================================================
|
||
|
||
Write-Information ""
|
||
Write-Information "========================================"
|
||
Write-Information " pptopic 一键安装程序"
|
||
Write-Information "========================================"
|
||
Write-Information ""
|
||
Write-Information "项目目录:$ProjectRoot"
|
||
|
||
if ($SkipPngquant) {
|
||
Write-Information "已指定 -SkipPngquant,将跳过 pngquant 安装。"
|
||
}
|
||
if ($Force) {
|
||
Write-Information "已指定 -Force,将强制重新安装 uv / pngquant。"
|
||
}
|
||
|
||
# 第一步:安装 uv
|
||
Invoke-Step -Title "步骤 1/5:安装 uv" -Action {
|
||
$uvPath = Test-CommandExists -Name "uv"
|
||
|
||
if ($uvPath -and -not $Force) {
|
||
Write-Information "uv 已存在:$uvPath"
|
||
} else {
|
||
$installer = Join-Path $ProjectRoot "uv-installer.ps1"
|
||
if (-not (Test-Path $installer)) {
|
||
throw "找不到 uv 安装脚本:$installer"
|
||
}
|
||
|
||
Write-Information "正在运行 uv-installer.ps1..."
|
||
& $installer
|
||
}
|
||
|
||
# uv-installer.ps1 会修改注册表 PATH,但当前会话不会立即生效,需要手动加入
|
||
$uvInstallDir = Get-UvInstallDir
|
||
Add-ToCurrentPath -LiteralPath $uvInstallDir
|
||
|
||
# 再次验证
|
||
$uvPath = Test-CommandExists -Name "uv"
|
||
if (-not $uvPath) {
|
||
throw "uv 安装后仍无法在当前会话中找到,请尝试重启终端后重试。"
|
||
}
|
||
|
||
Write-Information "uv 路径:$uvPath"
|
||
}
|
||
|
||
# 第二步:安装 Python 3.13
|
||
Invoke-Step -Title "步骤 2/5:安装 Python 3.13" -Action {
|
||
Write-Information "正在使用 uv 安装 Python 3.13..."
|
||
# uv 在 Python 已安装时会向 stderr 输出 "Python 3.13 is already installed"(退出码仍为 0),
|
||
# 通过 Invoke-NativeCommand 绕过 Stop 模式对 stderr 的误判。
|
||
$output = Invoke-NativeCommand -LiteralCommand "uv python install 3.13" -FailureMessage "uv python install 失败"
|
||
Write-Information ($output -join "`n")
|
||
Write-Information "Python 安装完成。"
|
||
}
|
||
|
||
# 第三步:安装 pngquant
|
||
if (-not $SkipPngquant) {
|
||
Invoke-Step -Title "步骤 3/5:安装 pngquant" -Action {
|
||
$pngquantPath = Test-CommandExists -Name "pngquant"
|
||
|
||
if ($pngquantPath -and -not $Force) {
|
||
Write-Information "pngquant 已存在:$pngquantPath"
|
||
} else {
|
||
$installer = Join-Path $ProjectRoot "install-pngquant.ps1"
|
||
if (-not (Test-Path $installer)) {
|
||
throw "找不到 pngquant 安装脚本:$installer"
|
||
}
|
||
|
||
if ($Force) {
|
||
Write-Information "正在运行 install-pngquant.ps1 -Force..."
|
||
& $installer -Force
|
||
} else {
|
||
Write-Information "正在运行 install-pngquant.ps1..."
|
||
& $installer
|
||
}
|
||
}
|
||
|
||
# install-pngquant.ps1 内部已刷新当前会话 PATH,这里再做一次保险
|
||
# 覆盖默认安装位置和 scoop 安装位置两种情况
|
||
Add-ToCurrentPath -LiteralPath (Get-PngquantInstallDir)
|
||
|
||
$pngquantPath = Test-CommandExists -Name "pngquant"
|
||
if (-not $pngquantPath) {
|
||
throw "pngquant 安装后仍无法在当前会话中找到,请尝试重启终端后重试。"
|
||
}
|
||
|
||
Write-Information "pngquant 路径:$pngquantPath"
|
||
}
|
||
} else {
|
||
Write-Information ""
|
||
Write-Information "========================================"
|
||
Write-Information " 步骤 3/5:跳过 pngquant 安装"
|
||
Write-Information "========================================"
|
||
}
|
||
|
||
# 第四步:安装 pptopic
|
||
if (-not $SkipPptopicInstall) {
|
||
Invoke-Step -Title "步骤 4/5:安装 pptopic" -Action {
|
||
$pyproject = Join-Path $ProjectRoot "pyproject.toml"
|
||
if (-not (Test-Path $pyproject)) {
|
||
throw "找不到 pyproject.toml:$pyproject"
|
||
}
|
||
|
||
Write-Information "正在使用 uv tool install -e $ProjectRoot 安装 pptopic..."
|
||
# uv 的进度/解析信息会写 stderr,通过 Invoke-NativeCommand 绕过 Stop 模式误判。
|
||
$output = Invoke-NativeCommand -LiteralCommand "uv tool install -e `"$ProjectRoot`"" -FailureMessage "uv tool install 失败"
|
||
Write-Information ($output -join "`n")
|
||
Write-Information "pptopic 安装完成。"
|
||
}
|
||
} else {
|
||
Write-Information ""
|
||
Write-Information "========================================"
|
||
Write-Information " 步骤 4/5:跳过 pptopic 安装"
|
||
Write-Information "========================================"
|
||
}
|
||
|
||
# 第五步:验证
|
||
Invoke-Step -Title "步骤 5/5:验证安装" -Action {
|
||
# 版本命令可能写到 stderr(部分 CLI 行为不一致),
|
||
# 在 Stop 模式下会误报失败,统一通过 Invoke-NativeCommand 安全调用。
|
||
|
||
Write-Information ""
|
||
Write-Information "--- uv 版本 ---"
|
||
$v = Invoke-NativeCommand -LiteralCommand "uv --version" -FailureMessage "uv 版本查询失败"
|
||
Write-Information ($v -join "`n")
|
||
|
||
if (-not $SkipPngquant) {
|
||
Write-Information ""
|
||
Write-Information "--- pngquant 版本 ---"
|
||
$v = Invoke-NativeCommand -LiteralCommand "pngquant --version" -FailureMessage "pngquant 版本查询失败"
|
||
Write-Information ($v -join "`n")
|
||
}
|
||
|
||
if (-not $SkipPptopicInstall) {
|
||
Write-Information ""
|
||
Write-Information "--- pptopic 版本 ---"
|
||
$v = Invoke-NativeCommand -LiteralCommand "pptopic version" -FailureMessage "pptopic 版本查询失败"
|
||
Write-Information ($v -join "`n")
|
||
}
|
||
|
||
# PowerPoint 是运行时必需依赖(win32com 调用),但安装阶段无法自动安装,
|
||
# 这里只做检测并给出明确提示,把缺失问题前置,避免用户装完后才发现用不了。
|
||
Write-Information ""
|
||
Write-Information "--- PowerPoint 检测 ---"
|
||
if (Test-PowerPointInstalled) {
|
||
Write-Information "PowerPoint 已安装(检测到 PowerPoint.Application 注册项)。"
|
||
} else {
|
||
Write-Information "警告:未检测到 PowerPoint。"
|
||
Write-Information "pptopic 导出 PPTX 时需要调用 PowerPoint(Office 2016 或更新版本)。"
|
||
Write-Information "请先安装 PowerPoint,否则 'pptopic export' 命令将无法运行。"
|
||
}
|
||
}
|
||
|
||
# ============================================================
|
||
# 完成
|
||
# ============================================================
|
||
|
||
Write-Information ""
|
||
Write-Information "========================================"
|
||
Write-Information " pptopic 一键安装完成!"
|
||
Write-Information "========================================"
|
||
Write-Information ""
|
||
|
||
if (-not $SkipPptopicInstall) {
|
||
Write-Information "你可以立即在当前窗口使用以下命令:"
|
||
Write-Information " pptopic export 你的文件.pptx"
|
||
Write-Information " pptopic export 你的文件.pptx --optimize --max-height 29999 -o result.png"
|
||
}
|
||
|
||
Write-Information ""
|
||
Write-Information "提示:本脚本已自动刷新当前 PowerShell 会话的 PATH,无需重启终端。"
|
||
Write-Information "如果关闭此窗口后在新窗口中找不到命令,请重新运行一次 setup.bat。"
|
||
Write-Information ""
|