#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 ""