diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..459ed0f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +**Important:** Use Chinese for information responses and thinking; use English for searching and querying. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. \ No newline at end of file diff --git a/README.md b/README.md index 229a7ae..f284340 100644 --- a/README.md +++ b/README.md @@ -9,22 +9,115 @@ - 支持自定义输出宽度和目录 - 内置图片优化功能,使用图片优化引擎进行无损压缩 -## 安装 +## 新手使用说明 -使用uv进行基本安装和工具设置。必要工具,推荐使用scoop进行安装。 +如果你是第一次使用命令行工具,请按以下步骤操作。 + +### 准备工作 + +- **PowerPoint**:需要安装 Microsoft PowerPoint(Office 2016 或更新版本) +- **网络连接**:安装过程中需要下载工具 + +### 第一步:安装 uv + +`uv` 是一个 Python 包管理器,用来安装 pptopic 及其依赖。 + +打开 PowerShell(在开始菜单搜索 "PowerShell"),进入本项目的目录: + +```powershell +cd pptopic +``` + +运行项目自带的安装脚本: + +```powershell +.\uv-installer.ps1 +``` + +该脚本会自动下载最新版本的 uv 并添加到系统 PATH。如果你在中国大陆,脚本已内置 GitHub 加速镜像,无需额外配置。 + +安装完成后,**关闭并重新打开 PowerShell**,验证安装: + +```powershell +uv --version +``` + +> 如果你已安装 [scoop](https://scoop.sh/),也可以直接 `scoop install uv`。 +> 如果想安装特定版本的 uv,可以在运行脚本前设置环境变量:`$env:UV_INSTALLER_VERSION = "0.11.20"` + +### 第二步:安装 Python + +使用 uv 安装 Python 3.13 或更新版本: + +```powershell +uv python install 3.13 +``` + +### 第三步:安装 pptopic + +```powershell +uv pip install -e . +``` + +### 第四步(可选):安装 pngquant 图片优化工具 + +虽然是可选安装,但我超级建议你安装,因为ppt导出后,图片通常较大,不进行图片无损压缩,会导致文件大小过大。 + +如果你需要对导出的图片进行压缩优化,运行: + +```powershell +.\install-pngquant.ps1 +``` + +该脚本会: +1. 如果你已安装 scoop → 通过 scoop 安装 pngquant +2. 如果没有 scoop → 自动下载并安装到 `%APPDATA%\pngquant`,并添加到 PATH + +安装完成后,**重新打开 PowerShell**,验证安装: + +```powershell +pngquant --version +``` + +> 自定义安装目录:`.\install-pngquant.ps1 -InstallDir "D:\Tools\pngquant"` +> 强制重新安装:`.\install-pngquant.ps1 -Force` + +### 第五步:开始使用 + +```powershell +# 导出 PPTX 为长图 +pptopic export presentation.pptx + +# 优化图片 +pptopic optimize image.png +``` + +至此安装完成。如果任何步骤遇到问题,请参考下方详细说明。 + +## 安装 ### 安装 uv +使用项目自带的安装脚本(推荐): + +```powershell +.\uv-installer.ps1 +``` + +该脚本自动检测 uv 最新版本,支持国内 GitHub 加速镜像下载(`mirror.ghproxy.com`、`ghproxy.net`),并自动将 uv 添加到系统 PATH。 + +也可以通过 scoop 安装: + ```bash -# 使用 scoop scoop install uv ``` ### 安装 pptopic ```bash -$ cd pptopic -$ uv pip install -e . +cd pptopic +uv sync +uv pip install -e . ``` ## 使用 @@ -67,21 +160,30 @@ pptopic optimize image.png --max-height 20000 --engine pngquant ## 图片优化 为了获得最佳的图片压缩效果,推荐使用 [pngquant](https://pngquant.org/) 进行图片压缩。 -当然,使用自己熟悉的图片优化压缩引擎是一样的。 ### 自动安装脚本 -使用提供的 PowerShell 脚本自动安装 pngquant: +使用提供的 PowerShell 脚本安装 pngquant: ```powershell .\install-pngquant.ps1 ``` -该脚本会自动: -1. 检查 pngquant 是否已安装 -2. 如果未安装,检查 scoop 是否已安装 -3. 如果 scoop 未安装,自动安装 scoop -4. 使用 scoop 安装 pngquant +该脚本支持两种安装方式: + +1. **已安装 scoop** → 通过 `scoop install pngquant` 安装 +2. **未安装 scoop** → 从 pngquant 官网下载并安装到 `%APPDATA%\pngquant`,自动添加到 PATH + +```powershell +# 自定义安装目录 +.\install-pngquant.ps1 -InstallDir "D:\Tools\pngquant" + +# 强制重新安装 +.\install-pngquant.ps1 -Force + +# 不修改 PATH +.\install-pngquant.ps1 -NoModifyPath +``` ### 手动安装 @@ -89,6 +191,8 @@ pptopic optimize image.png --max-height 20000 --engine pngquant scoop install pngquant ``` +或从 [pngquant.org](https://pngquant.org/) 下载 Windows 版本手动配置。 + ## License -MIT License \ No newline at end of file +MIT License diff --git a/install-pngquant.ps1 b/install-pngquant.ps1 index d77012c..4d8c371 100644 --- a/install-pngquant.ps1 +++ b/install-pngquant.ps1 @@ -1,8 +1,11 @@ param( - [switch]$Force + [switch]$Force, + [string]$InstallDir = "$env:APPDATA\pngquant", + [switch]$NoModifyPath ) $ErrorActionPreference = "Stop" +$DownloadUrl = "https://pngquant.org/pngquant-windows.zip" function Test-PngquantInstalled { try { @@ -21,51 +24,102 @@ function Test-ScoopInstalled { Write-Host "scoop 已安装: $($result.Source)" -ForegroundColor Green return $true } catch { - Write-Host "scoop 未安装" -ForegroundColor Yellow return $false } } -function Install-Scoop { - Write-Host "正在安装 scoop..." -ForegroundColor Cyan - - $scoopInstallScript = "irm get.scoop.sh | iex" - - try { - Invoke-Expression $scoopInstallScript - Write-Host "scoop 安装成功" -ForegroundColor Green - - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","User") + ";" + [System.Environment]::GetEnvironmentVariable("Path","Machine") - - return $true - } catch { - Write-Host "scoop 安装失败: $_" -ForegroundColor Red - return $false - } -} - -function Install-Pngquant { +function Install-ViaScoop { Write-Host "正在使用 scoop 安装 pngquant..." -ForegroundColor Cyan - try { scoop install pngquant Write-Host "pngquant 安装成功" -ForegroundColor Green return $true } catch { - Write-Host "pngquant 安装失败: $_" -ForegroundColor Red + Write-Host "scoop 安装 pngquant 失败: $_" -ForegroundColor Red return $false } } +function Install-Manual { + Write-Host "正在从 $DownloadUrl 下载 pngquant..." -ForegroundColor Cyan + + $zipPath = "$env:TEMP\pngquant-windows.zip" + $extractTemp = "$env:TEMP\pngquant_extract" + + try { + # Download + $wc = New-Object System.Net.WebClient + $wc.DownloadFile($DownloadUrl, $zipPath) + + # Clean up previous temp extract + if (Test-Path $extractTemp) { + Remove-Item $extractTemp -Recurse -Force + } + + # Extract + Expand-Archive -Path $zipPath -DestinationPath $extractTemp + + # Create install dir + if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + } + + # Copy exe files to install dir + $exeFiles = Get-ChildItem -Path $extractTemp -Filter "*.exe" -Recurse + foreach ($exe in $exeFiles) { + Copy-Item -Path $exe.FullName -Destination $InstallDir -Force + Write-Host " 已安装: $($exe.Name)" -ForegroundColor Green + } + + # Cleanup + Remove-Item $zipPath -Force -ErrorAction SilentlyContinue + Remove-Item $extractTemp -Recurse -Force -ErrorAction SilentlyContinue + + # Add to PATH + if (-not $NoModifyPath) { + Add-ToPath $InstallDir + } + + return $true + } catch { + Write-Host "手动安装失败: $_" -ForegroundColor Red + return $false + } +} + +function Add-ToPath($LiteralPath) { + $RegistryPath = 'registry::HKEY_CURRENT_USER\Environment' + $CurrentDirs = (Get-Item -LiteralPath $RegistryPath).GetValue( + 'Path', '', 'DoNotExpandEnvironmentNames' + ) -split ';' -ne '' + + if ($LiteralPath -in $CurrentDirs) { + Write-Host "安装目录已在 PATH 中: $LiteralPath" -ForegroundColor Green + return + } + + $NewPath = (,$LiteralPath + $CurrentDirs) -join ';' + Set-ItemProperty -Type ExpandString -LiteralPath $RegistryPath Path $NewPath + + # 通知系统环境变量已更新 + $DummyName = 'pngquant-install-' + [guid]::NewGuid().ToString() + [Environment]::SetEnvironmentVariable($DummyName, 'dummy', 'User') + [Environment]::SetEnvironmentVariable($DummyName, [NullString]::value, 'User') + + Write-Host "已将 $LiteralPath 添加到用户 PATH" -ForegroundColor Green +} + +# ============================================================ Write-Host "========================================" -ForegroundColor Cyan Write-Host " pngquant 安装脚本" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" if (Test-PngquantInstalled) { - Write-Host "" - Write-Host "pngquant 已经安装,无需重复安装。" -ForegroundColor Green if (-not $Force) { + Write-Host "" + Write-Host "pngquant 已经安装,无需重复安装。" -ForegroundColor Green + Write-Host "如需强制重新安装,请使用 -Force 参数。" -ForegroundColor Yellow exit 0 } Write-Host "使用 -Force 参数强制重新安装..." -ForegroundColor Yellow @@ -73,42 +127,40 @@ if (Test-PngquantInstalled) { Write-Host "" -if (-not (Test-ScoopInstalled)) { - Write-Host "" - Write-Host "scoop 未安装,正在安装 scoop..." -ForegroundColor Yellow - - if (-not (Install-Scoop)) { +if (Test-ScoopInstalled) { + Write-Host "检测到 scoop,使用 scoop 安装..." -ForegroundColor Cyan + if (Install-ViaScoop) { Write-Host "" - Write-Host "scoop 安装失败,请手动安装后重试。" -ForegroundColor Red - Write-Host "手动安装命令: irm get.scoop.sh | iex" -ForegroundColor Yellow - exit 1 + pngquant --version + exit 0 } - - Write-Host "" - Write-Host "请重新运行此脚本以继续安装 pngquant。" -ForegroundColor Yellow - Write-Host "或者手动运行: scoop install pngquant" -ForegroundColor Yellow - exit 0 + Write-Host "scoop 安装失败,回退到手动安装方式..." -ForegroundColor Yellow +} else { + Write-Host "未检测到 scoop,使用手动安装方式..." -ForegroundColor Cyan } +Write-Host "安装目录: $InstallDir" -ForegroundColor Cyan Write-Host "" -if (Install-Pngquant) { +if (Install-Manual) { Write-Host "" Write-Host "========================================" -ForegroundColor Green Write-Host " pngquant 安装完成!" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green - - $pngquantPath = Get-Command pngquant -ErrorAction SilentlyContinue - if ($pngquantPath) { - Write-Host "安装路径: $($pngquantPath.Source)" -ForegroundColor Cyan + Write-Host "安装路径: $InstallDir" -ForegroundColor Cyan + + try { + $env:Path = "$InstallDir;$env:Path" + Write-Host "" + Write-Host "验证安装:" -ForegroundColor Cyan + pngquant --version + } catch { + Write-Host "" + Write-Host "安装文件已就绪,请重启终端后运行 pngquant --version 验证。" -ForegroundColor Yellow } - - Write-Host "" - Write-Host "验证安装:" -ForegroundColor Cyan - pngquant --version } else { Write-Host "" Write-Host "pngquant 安装失败。" -ForegroundColor Red - Write-Host "请尝试手动安装: scoop install pngquant" -ForegroundColor Yellow + Write-Host "请尝试手动下载: $DownloadUrl" -ForegroundColor Yellow exit 1 } diff --git a/uv-installer.ps1 b/uv-installer.ps1 new file mode 100644 index 0000000..180f7f1 --- /dev/null +++ b/uv-installer.ps1 @@ -0,0 +1,683 @@ +# Licensed under the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +<# +.SYNOPSIS + +The installer for uv (auto-detects latest release) + +.DESCRIPTION + +This script auto-detects the latest uv release from GitHub and fetches an appropriate archive from +various download sources, then unpacks the binaries and installs them to the first of the following locations + + $env:XDG_BIN_HOME + $env:XDG_DATA_HOME/../bin + $HOME/.local/bin + +It will then add that dir to PATH by editing your Environment.Path registry key + +.PARAMETER NoModifyPath +Don't add the install directory to PATH + +.PARAMETER Help +Print help + +#> + +param ( + [Parameter(HelpMessage = "Don't add the install directory to PATH")] + [switch]$NoModifyPath, + [Parameter(HelpMessage = "Print Help")] + [switch]$Help +) +function Get-LatestVersion { + if ($env:UV_INSTALLER_VERSION) { + return $env:UV_INSTALLER_VERSION + } + + $fallback_version = "0.11.20" + + try { + $http = New-Object System.Net.Http.HttpClient + $http.Timeout = [TimeSpan]::FromSeconds(5) + $http.DefaultRequestHeaders.Add("User-Agent", "PowerShell/uv-installer") + $http.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json") + $response = $http.GetStringAsync("https://api.github.com/repos/astral-sh/uv/releases/latest").Result + $json = $response | ConvertFrom-Json + $version = $json.tag_name + if ($version) { + Write-Information "Latest uv version: $version" + return $version + } + } catch { + Write-Information "Could not reach GitHub API, using fallback version $fallback_version" + } + + return $fallback_version +} + +$app_name = 'uv' +$app_version = Get-LatestVersion +if ($env:UV_DOWNLOAD_URL) { + $ArtifactDownloadUrls = @($env:UV_DOWNLOAD_URL) +} elseif ($env:INSTALLER_DOWNLOAD_URL) { + $ArtifactDownloadUrls = @($env:INSTALLER_DOWNLOAD_URL) +} elseif ($env:UV_INSTALLER_GHE_BASE_URL) { + $installer_base_url = $env:UV_INSTALLER_GHE_BASE_URL + $ArtifactDownloadUrls = @("$installer_base_url/astral-sh/uv/releases/download/$app_version") +} elseif ($env:UV_INSTALLER_GITHUB_BASE_URL) { + $installer_base_url = $env:UV_INSTALLER_GITHUB_BASE_URL + $ArtifactDownloadUrls = @("$installer_base_url/astral-sh/uv/releases/download/$app_version") +} else { + $ArtifactDownloadUrls = @( + "https://releases.astral.sh/github/uv/releases/download/$app_version", + "https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/download/$app_version", + "https://ghproxy.net/https://github.com/astral-sh/uv/releases/download/$app_version", + "https://github.com/astral-sh/uv/releases/download/$app_version" + ) +} + +$auth_token = $env:UV_GITHUB_TOKEN + +$receipt = @" +{"binaries":["CARGO_DIST_BINS"],"binary_aliases":{},"cdylibs":["CARGO_DIST_DYLIBS"],"cstaticlibs":["CARGO_DIST_STATICLIBS"],"install_layout":"unspecified","install_prefix":"AXO_INSTALL_PREFIX","modify_path":true,"provider":{"source":"cargo-dist","version":"0.31.0"},"source":{"app_name":"uv","name":"uv","owner":"astral-sh","release_type":"github"},"version":"$app_version"} +"@ +if ($env:XDG_CONFIG_HOME) { + $receipt_home = "${env:XDG_CONFIG_HOME}\uv" +} else { + $receipt_home = "${env:LOCALAPPDATA}\uv" +} + +if ($env:UV_DISABLE_UPDATE) { + $install_updater = $false +} else { + $install_updater = $true +} + +if ($NoModifyPath) { + Write-Information "-NoModifyPath has been deprecated; please set UV_NO_MODIFY_PATH=1 in the environment" +} + +if ($env:UV_NO_MODIFY_PATH) { + $NoModifyPath = $true +} + +$unmanaged_install = $env:UV_UNMANAGED_INSTALL + +if ($unmanaged_install) { + $NoModifyPath = $true + $install_updater = $false +} + +function Install-Binary($install_args) { + if ($Help) { + Get-Help $PSCommandPath -Detailed + Exit + } + + Initialize-Environment + + # Platform info injected by dist + $platforms = @{ + "aarch64-pc-windows-gnu" = @{ + "artifact_name" = "uv-aarch64-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + "aarch64-pc-windows-msvc" = @{ + "artifact_name" = "uv-aarch64-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + "i686-pc-windows-gnu" = @{ + "artifact_name" = "uv-i686-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + "i686-pc-windows-msvc" = @{ + "artifact_name" = "uv-i686-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + "x86_64-pc-windows-gnu" = @{ + "artifact_name" = "uv-x86_64-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + "x86_64-pc-windows-msvc" = @{ + "artifact_name" = "uv-x86_64-pc-windows-msvc.zip" + "bins" = @("uv.exe", "uvx.exe", "uvw.exe") + "libs" = @() + "staticlibs" = @() + "zip_ext" = ".zip" + "aliases" = @{ + } + "aliases_json" = '{}' + } + } + + $arch = Get-TargetTriple $platforms + if (-not $platforms.ContainsKey($arch)) { + $platforms_json = ConvertTo-Json $platforms + throw "ERROR: could not find binaries for this platform. Last platform tried: $arch platform info: $platforms_json" + } + Write-Information "downloading $app_name $app_version ($arch)" + + $download_result = $false + $first_url = $true + foreach ($url in $ArtifactDownloadUrls) { + if (-not $first_url) { + Write-Information "trying alternative download URL" + } + $first_url = $false + + try { + $fetched = Download -download_url "$url" -platforms $platforms -arch $arch + $download_result = $true + break + } catch { + Write-Information "failed to download from $url" + # keep going, maybe we have backup download URLs + } + } + if (-not $download_result) { + throw "failed to download binaries" + } + + # FIXME: add a flag that lets the user not do this step + try { + Invoke-Installer -artifacts $fetched -platforms $platforms "$install_args" + } catch { + throw @" +We encountered an error trying to perform the installation; +please review the error messages below. + +$_ +"@ + } +} + +function Get-TargetTriple($platforms) { + $double = Get-Arch + if ($platforms.Contains("$double-msvc")) { + return "$double-msvc" + } else { + return "$double-gnu" + } +} + +function Get-Arch() { + try { + # NOTE: this might return X64 on ARM64 Windows, which is OK since emulation is available. + # It works correctly starting in PowerShell Core 7.3 and Windows PowerShell in Win 11 22H2. + # Ideally this would just be + # [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + # but that gets a type from the wrong assembly on Windows PowerShell (i.e. not Core) + $a = [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices.RuntimeInformation") + $t = $a.GetType("System.Runtime.InteropServices.RuntimeInformation") + $p = $t.GetProperty("OSArchitecture") + # Possible OSArchitecture Values: https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.architecture + # Rust supported platforms: https://doc.rust-lang.org/stable/rustc/platform-support.html + switch ($p.GetValue($null).ToString()) + { + "X86" { return "i686-pc-windows" } + "X64" { return "x86_64-pc-windows" } + "Arm" { return "thumbv7a-pc-windows" } + "Arm64" { return "aarch64-pc-windows" } + } + } catch { + # The above was added in .NET 4.7.1, so Windows PowerShell in versions of Windows + # prior to Windows 10 v1709 may not have this API. + Write-Verbose "Get-TargetTriple: Exception when trying to determine OS architecture." + Write-Verbose $_ + } + + # This is available in .NET 4.0. We already checked for PS 5, which requires .NET 4.5. + Write-Verbose("Get-TargetTriple: falling back to Is64BitOperatingSystem.") + if ([System.Environment]::Is64BitOperatingSystem) { + return "x86_64-pc-windows" + } else { + return "i686-pc-windows" + } +} + +function WebProxyFromUrl { + param([string]$ProxyUrl) + + if ([string]::IsNullOrWhiteSpace($ProxyUrl)) { + return $null + } + + try { + # Parse the proxy URL + $uri = [System.Uri]$ProxyUrl + + # Create WebProxy instance + $webProxy = New-Object System.Net.WebProxy($uri) + + # Set credentials if provided in URL + if (-not [string]::IsNullOrEmpty($uri.UserInfo)) { + $userInfo = $uri.UserInfo.Split(':') + $username = [System.Uri]::UnescapeDataString($userInfo[0]) + $password = if ($null -eq $userInfo[1]) { "" } else { [System.Uri]::UnescapeDataString($userInfo[1]) } + $webProxy.Credentials = New-Object System.Net.NetworkCredential($username, $password) + } + + return $webProxy + } + catch { + Write-Verbose("Failed to parse proxy URL '$ProxyUrl': $($_.Exception.Message)") + return $null + } +} + +function WebProxyFromEnvironment { + $httpsProxy = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY") + $allProxy = [System.Environment]::GetEnvironmentVariable("ALL_PROXY") + $proxyUrl = if (-not [string]::IsNullOrWhiteSpace($httpsProxy)) { $httpsProxy } else { $allProxy } + $webProxy = WebProxyFromUrl -ProxyUrl $proxyUrl + return $webProxy +} + +function Download($download_url, $platforms, $arch) { + # Lookup what we expect this platform to look like + $info = $platforms[$arch] + $zip_ext = $info["zip_ext"] + $bin_names = $info["bins"] + $lib_names = $info["libs"] + $staticlib_names = $info["staticlibs"] + $artifact_name = $info["artifact_name"] + + # Make a new temp dir to unpack things to + $tmp = New-Temp-Dir + $dir_path = "$tmp\$app_name$zip_ext" + + # Download and unpack! + $url = "$download_url/$artifact_name" + Write-Verbose " from $url" + Write-Verbose " to $dir_path" + $wc = New-Object Net.Webclient + $proxy = WebProxyFromEnvironment + if ($null -ne $proxy) { + $wc.Proxy = $proxy + } + if ($auth_token) { + $wc.Headers["Authorization"] = "Bearer $auth_token" + } + $wc.downloadFile($url, $dir_path) + + Write-Verbose "Unpacking to $tmp" + + # Select the tool to unpack the files with. + # + # As of windows 10(?), powershell comes with tar preinstalled, but in practice + # it only seems to support .tar.gz, and not xz/zstd. Still, we should try to + # forward all tars to it in case the user has a machine that can handle it! + switch -Wildcard ($zip_ext) { + ".zip" { + Expand-Archive -Path $dir_path -DestinationPath "$tmp"; + Break + } + ".tar.*" { + tar xf $dir_path --strip-components 1 -C "$tmp"; + Break + } + Default { + throw "ERROR: unknown archive format $zip_ext" + } + } + + # Let the next step know what to copy + $bin_paths = @() + foreach ($bin_name in $bin_names) { + Write-Verbose " Unpacked $bin_name" + $bin_paths += "$tmp\$bin_name" + } + $lib_paths = @() + foreach ($lib_name in $lib_names) { + Write-Verbose " Unpacked $lib_name" + $lib_paths += "$tmp\$lib_name" + } + $staticlib_paths = @() + foreach ($lib_name in $staticlib_names) { + Write-Verbose " Unpacked $lib_name" + $staticlib_paths += "$tmp\$lib_name" + } + + if (($null -ne $info["updater"]) -and $install_updater) { + $updater_id = $info["updater"]["artifact_name"] + $updater_url = "$download_url/$updater_id" + $out_name = "$tmp\uv-update.exe" + + $wc.downloadFile($updater_url, $out_name) + $bin_paths += $out_name + } + + return @{ + "bin_paths" = $bin_paths + "lib_paths" = $lib_paths + "staticlib_paths" = $staticlib_paths + } +} + +function Invoke-Installer($artifacts, $platforms) { + # Replaces the placeholder binary entry with the actual list of binaries + $arch = Get-TargetTriple $platforms + + if (-not $platforms.ContainsKey($arch)) { + $platforms_json = ConvertTo-Json $platforms + throw "ERROR: could not find binaries for this platform. Last platform tried: $arch platform info: $platforms_json" + } + + $info = $platforms[$arch] + + # Forces the install to occur at this path, not the default + $force_install_dir = $null + $install_layout = "unspecified" + # Check the newer app-specific variable before falling back + # to the older generic one + if (($env:UV_INSTALL_DIR)) { + $force_install_dir = $env:UV_INSTALL_DIR + $install_layout = "flat" + } elseif (($env:CARGO_DIST_FORCE_INSTALL_DIR)) { + $force_install_dir = $env:CARGO_DIST_FORCE_INSTALL_DIR + $install_layout = "flat" + } elseif ($unmanaged_install) { + $force_install_dir = $unmanaged_install + $install_layout = "flat" + } + + # Check if the install layout should be changed from `flat` to `cargo-home` + # for backwards compatible updates of applications that switched layouts. + if (($force_install_dir) -and ($install_layout -eq "flat")) { + # If the install directory is targeting the Cargo home directory, then + # we assume this application was previously installed that layout + # Note the installer passes the path with `\\` separators, but here they are + # `\` so we normalize for comparison. We don't use `Resolve-Path` because they + # may not exist. + $cargo_home = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { + Join-Path $(if ($HOME) { $HOME } else { "." }) ".cargo" + } + if ($force_install_dir.Replace('\\', '\') -eq $cargo_home) { + $install_layout = "cargo-home" + } + } + + # The actual path we're going to install to + $dest_dir = $null + $dest_dir_lib = $null + # The install prefix we write to the receipt. + # For organized install methods like CargoHome, which have + # subdirectories, this is the root without `/bin`. For other + # methods, this is the same as `_install_dir`. + $receipt_dest_dir = $null + # Before actually consulting the configured install strategy, see + # if we're overriding it. + if (($force_install_dir)) { + switch ($install_layout) { + "hierarchical" { + $dest_dir = Join-Path $force_install_dir "bin" + $dest_dir_lib = Join-Path $force_install_dir "lib" + } + "cargo-home" { + $dest_dir = Join-Path $force_install_dir "bin" + $dest_dir_lib = $dest_dir + } + "flat" { + $dest_dir = $force_install_dir + $dest_dir_lib = $dest_dir + } + Default { + throw "Error: unrecognized installation layout: $install_layout" + } + } + $receipt_dest_dir = $force_install_dir + } + if (-Not $dest_dir) { + # Install to $env:XDG_BIN_HOME + $dest_dir = if (($base_dir = $env:XDG_BIN_HOME)) { + Join-Path $base_dir "" + } + $dest_dir_lib = $dest_dir + $receipt_dest_dir = $dest_dir + $install_layout = "flat" + } + if (-Not $dest_dir) { + # Install to $env:XDG_DATA_HOME/../bin + $dest_dir = if (($base_dir = $env:XDG_DATA_HOME)) { + Join-Path $base_dir "../bin" + } + $dest_dir_lib = $dest_dir + $receipt_dest_dir = $dest_dir + $install_layout = "flat" + } + if (-Not $dest_dir) { + # Install to $HOME/.local/bin + $dest_dir = if (($base_dir = $HOME)) { + Join-Path $base_dir ".local/bin" + } + $dest_dir_lib = $dest_dir + $receipt_dest_dir = $dest_dir + $install_layout = "flat" + } + + # Looks like all of the above assignments failed + if (-Not $dest_dir) { + throw "ERROR: could not find a valid path to install to; please check the installation instructions" + } + + # The replace call here ensures proper escaping is inlined into the receipt + $receipt = $receipt.Replace('AXO_INSTALL_PREFIX', $receipt_dest_dir.replace("\", "\\")) + $receipt = $receipt.Replace('"install_layout":"unspecified"', -join('"install_layout":"', $install_layout, '"')) + + $dest_dir = New-Item -Force -ItemType Directory -Path $dest_dir + $dest_dir_lib = New-Item -Force -ItemType Directory -Path $dest_dir_lib + Write-Information "installing to $dest_dir" + # Just copy the binaries from the temp location to the install dir + foreach ($bin_path in $artifacts["bin_paths"]) { + $installed_file = Split-Path -Path "$bin_path" -Leaf + Copy-Item "$bin_path" -Destination "$dest_dir" -ErrorAction Stop + Remove-Item "$bin_path" -Recurse -Force -ErrorAction Stop + Write-Information " $installed_file" + + if (($dests = $info["aliases"][$installed_file])) { + $source = Join-Path "$dest_dir" "$installed_file" + foreach ($dest_name in $dests) { + $dest = Join-Path $dest_dir $dest_name + $null = New-Item -ItemType HardLink -Target "$source" -Path "$dest" -Force -ErrorAction Stop + } + } + } + foreach ($lib_path in $artifacts["lib_paths"]) { + $installed_file = Split-Path -Path "$lib_path" -Leaf + Copy-Item "$lib_path" -Destination "$dest_dir_lib" -ErrorAction Stop + Remove-Item "$lib_path" -Recurse -Force -ErrorAction Stop + Write-Information " $installed_file" + } + foreach ($lib_path in $artifacts["staticlib_paths"]) { + $installed_file = Split-Path -Path "$lib_path" -Leaf + Copy-Item "$lib_path" -Destination "$dest_dir_lib" -ErrorAction Stop + Remove-Item "$lib_path" -Recurse -Force -ErrorAction Stop + Write-Information " $installed_file" + } + + $formatted_bins = ($info["bins"] | ForEach-Object { '"' + $_ + '"' }) -join "," + $receipt = $receipt.Replace('"CARGO_DIST_BINS"', $formatted_bins) + $formatted_libs = ($info["libs"] | ForEach-Object { '"' + $_ + '"' }) -join "," + $receipt = $receipt.Replace('"CARGO_DIST_DYLIBS"', $formatted_libs) + $formatted_staticlibs = ($info["staticlibs"] | ForEach-Object { '"' + $_ + '"' }) -join "," + $receipt = $receipt.Replace('"CARGO_DIST_STATICLIBS"', $formatted_staticlibs) + # Also replace the aliases with the arch-specific one + $receipt = $receipt.Replace('"binary_aliases":{}', -join('"binary_aliases":', $info['aliases_json'])) + if ($NoModifyPath) { + $receipt = $receipt.Replace('"modify_path":true', '"modify_path":false') + } + + # Write the install receipt + if ($install_updater) { + $null = New-Item -Path $receipt_home -ItemType "directory" -ErrorAction SilentlyContinue + # Trying to get Powershell 5.1 (not 6+, which is fake and lies) to write utf8 is a crime + # because "Out-File -Encoding utf8" actually still means utf8BOM, so we need to pull out + # .NET's APIs which actually do what you tell them (also apparently utf8NoBOM is the + # default in newer .NETs but I'd rather not rely on that at this point). + $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False + [IO.File]::WriteAllLines("$receipt_home/uv-receipt.json", "$receipt", $Utf8NoBomEncoding) + } + + # Respect the environment, but CLI takes precedence + if ($null -eq $NoModifyPath) { + $NoModifyPath = $env:INSTALLER_NO_MODIFY_PATH + } + + Write-Information "everything's installed!" + if (-not $NoModifyPath) { + Add-Ci-Path $dest_dir + if (Add-Path $dest_dir) { + Write-Information "" + Write-Information "To add $dest_dir to your PATH, either restart your shell or run:" + Write-Information "" + Write-Information " set Path=$dest_dir;%Path% (cmd)" + Write-Information " `$env:Path = `"$dest_dir;`$env:Path`" (powershell)" + } + } +} + +# Attempt to do CI-specific rituals to get the install-dir on PATH faster +function Add-Ci-Path($OrigPathToAdd) { + # If GITHUB_PATH is present, then write install_dir to the file it refs. + # After each GitHub Action, the contents will be added to PATH. + # So if you put a curl | sh for this script in its own "run" step, + # the next step will have this dir on PATH. + # + # Note that GITHUB_PATH will not resolve any variables, so we in fact + # want to write the install dir and not an expression that evals to it + if (($gh_path = $env:GITHUB_PATH)) { + Write-Output "$OrigPathToAdd" | Out-File -FilePath "$gh_path" -Encoding utf8 -Append + } +} + +# Try to permanently add the given path to the user-level +# PATH via the registry +# +# Returns true if the registry was modified, otherwise returns false +# (indicating it was already on PATH) +# +# This is a lightly modified version of this solution: +# https://stackoverflow.com/questions/69236623/adding-path-permanently-to-windows-using-powershell-doesnt-appear-to-work/69239861#69239861 +function Add-Path($LiteralPath) { + Write-Verbose "Adding $LiteralPath to your user-level PATH" + + $RegistryPath = 'registry::HKEY_CURRENT_USER\Environment' + + # Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned. + # If 'Path' is not an existing item in the registry, '' is returned. + $CurrentDirectories = (Get-Item -LiteralPath $RegistryPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne '' + + if ($LiteralPath -in $CurrentDirectories) { + Write-Verbose "Install directory $LiteralPath already on PATH, all done!" + return $false + } + + Write-Verbose "Actually mutating 'Path' Property" + + # Add the new path to the front of the PATH. + # The ',' turns $LiteralPath into an array, which the array of + # $CurrentDirectories is then added to. + $NewPath = (,$LiteralPath + $CurrentDirectories) -join ';' + + # Update the registry. Will create the property if it did not already exist. + # Note the use of ExpandString to create a registry property with a REG_EXPAND_SZ data type. + Set-ItemProperty -Type ExpandString -LiteralPath $RegistryPath Path $NewPath + + # Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the + # updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation. + $DummyName = 'cargo-dist-' + [guid]::NewGuid().ToString() + [Environment]::SetEnvironmentVariable($DummyName, 'cargo-dist-dummy', 'User') + [Environment]::SetEnvironmentVariable($DummyName, [NullString]::value, 'User') + + Write-Verbose "Successfully added $LiteralPath to your user-level PATH" + return $true +} + +function Initialize-Environment() { + If (($PSVersionTable.PSVersion.Major) -lt 5) { + throw @" +Error: PowerShell 5 or later is required to install $app_name. +Upgrade PowerShell: + + https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell + +"@ + } + + # show notification to change execution policy: + $allowedExecutionPolicy = @('Unrestricted', 'RemoteSigned', 'Bypass') + If ((Get-ExecutionPolicy).ToString() -notin $allowedExecutionPolicy) { + throw @" +Error: PowerShell requires an execution policy in [$($allowedExecutionPolicy -join ", ")] to run $app_name. For example, to set the execution policy to 'RemoteSigned' please run: + + Set-ExecutionPolicy RemoteSigned -scope CurrentUser + +"@ + } + + # GitHub requires TLS 1.2 + If ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls12') { + throw @" +Error: Installing $app_name requires at least .NET Framework 4.5 +Please download and install it first: + + https://www.microsoft.com/net/download + +"@ + } +} + +function New-Temp-Dir() { + [CmdletBinding(SupportsShouldProcess)] + param() + $parent = [System.IO.Path]::GetTempPath() + [string] $name = [System.Guid]::NewGuid() + New-Item -ItemType Directory -Path (Join-Path $parent $name) +} + +# PSScriptAnalyzer doesn't like how we use our params as globals, this calms it +$Null = $ArtifactDownloadUrls, $NoModifyPath, $Help +# Make Write-Information statements be visible +$InformationPreference = "Continue" + +# The default interactive handler +try { + Install-Binary "$Args" +} catch { + Write-Information $_ + exit 1 +}