popy.psm1
# ---- ArgumentCompleter.ps1 ---- Register-ArgumentCompleter -Native -CommandName popy -ScriptBlock { param($word, $ast, $cursor) # 每次补全时重新读取配置 $configPath = Join-Path $env:UserProfile ".popy" $configFile = Join-Path $configPath 'config.json' if (Test-Path $configFile) { $cfg = Get-Content -LiteralPath $configFile -Raw | ConvertFrom-Json } $storePath = $cfg.storePath if (-not $storePath) { return } $versionsPath = Join-Path -Path $storePath -ChildPath "versions" $scriptsVersionPath = Join-Path -Path $storePath -ChildPath "scripts" $venvPath = Join-Path -Path $storePath -ChildPath "venvs" $downloadPath = Join-Path -Path $storePath -ChildPath "downloads" $requirementsPath = Join-Path -Path $storePath -ChildPath "requirements" $line = $ast.ToString() $parts = $line -split '\s+' switch -Regex ($parts[1]) { { $_ -in 'f', 'rfreeze' } { Get-ChildItem -Path $requirementsPath -Filter *.txt -Name | Where-Object { $_ -like "$word*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } break } { $_ -in 'a', 'activate' } { Get-ChildItem -Directory -Path $venvPath -Name | Where-Object { $_ -like "$word*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } break } } } # ---- Config.ps1 ---- function Set-Config { <# .SYNOPSIS 写入或更新配置项。 .EXAMPLE Set-Config -Key 'wix.version' -Value '3.14' Set-Config -Key 'mirrors' -Value @('https://a.com','https://b.com') #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Key, [Parameter(Mandatory)] [AllowNull()] $Value ) # 1. 统一读取现有配置(没有则初始化空对象) $cfg = Get-Configs if (-not $cfg) { $cfg = [PSCustomObject]@{} } # 2. 支持 a.b.c 的点分深度路径 $parts = $Key -split '\.' $node = $cfg for ($i = 0; $i -lt $parts.Count - 1; $i++) { $p = $parts[$i] if (-not $node.$p) { $node | Add-Member -NotePropertyName $p -NotePropertyValue ([PSCustomObject]@{}) -Force } $node = $node.$p } # 3. 设置最终值 $leaf = $parts[-1] if ($node.PSObject.Properties.Name -contains $leaf) { $node.$leaf = $Value } else { $node | Add-Member -NotePropertyName $leaf -NotePropertyValue $Value -Force } # 4. 保存文件(UTF-8 无 BOM,缩进友好) $popyConfigPath = (Get-ConfigsInternal).ConfigDir $configFile = Join-Path $popyConfigPath 'config.json' $cfg | ConvertTo-Json -Depth 10 | Out-File -LiteralPath $configFile -Encoding utf8 -Force } # ———— 内部帮助函数,避免重复计算路径 ———— function Get-ConfigsInternal { $configPath = Get-ConfigPath $configFile = Join-Path $configPath 'config.json' @{ ConfigDir = $configPath ConfigFile = $configFile } } # 兼容旧接口 function Get-Configs { $info = Get-ConfigsInternal if (Test-Path $info.ConfigFile) { return Get-Content -LiteralPath $info.ConfigFile -Raw | ConvertFrom-Json } } # ---- PathUtils.ps1 ---- function Get-RootPath { return Split-Path -Parent $PSScriptRoot } function Get-ConfigPath { return Join-Path $env:UserProfile ".popy" } function Get-StorePath { $cfg = Get-Configs return $cfg.storePath } function Get-ToolsPath { $popyConfigPath = Get-ConfigPath return Join-Path $popyConfigPath 'tools' } function New-Directory { param ( [Parameter(Mandatory = $false)] [string]$checkedPath, [Parameter(Mandatory = $false)] [string]$description ) if (-not(Test-Path -Path $checkedPath)) { try { New-Item -Path $checkedPath -ItemType Directory -ErrorAction Stop | Out-Null # Write-Host "$description 已创建:$checkedPath" -ForegroundColor Green } catch { Write-Host "创建 $description 时发生错误:$($_.Exception.Message)" -ForegroundColor Red exit } } } # ---- SaveFile.ps1 ---- #新版 function Save-File { param( [string]$TargetURL, [string]$SavePath ) # 1. 打开 TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # 2. 如果用户只给了目录,自动补文件名 if ([IO.Path]::HasExtension($SavePath) -eq $false) { $fileName = [IO.Path]::GetFileName($TargetURL) $SavePath = Join-Path $SavePath $fileName } # 3. 用 WebClient + 事件回调显示进度 $wc = New-Object System.Net.WebClient # 监听进度事件 Register-ObjectEvent -InputObject $wc -EventName DownloadProgressChanged -SourceIdentifier DL -Action { $percent = $EventArgs.ProgressPercentage Write-Progress -Activity "Downloading" -Status "$percent %" -PercentComplete $percent } | Out-Null # 监听完成事件 Register-ObjectEvent -InputObject $wc -EventName DownloadFileCompleted -SourceIdentifier DLC -Action { Write-Progress -Activity "Downloading" -Completed } | Out-Null try { Write-Host "正在下载 $([IO.Path]::GetFileName($TargetURL)) ..." $wc.DownloadFileAsync($TargetURL, $SavePath) # 等待异步完成 while ($wc.IsBusy) { Start-Sleep -Milliseconds 100 } if ((Get-Item $SavePath).Length -eq 0) { Remove-Item $SavePath -Force throw "下载文件大小为 0,可能 URL 无效。" } Write-Host "✅ 已保存到 $SavePath" -ForegroundColor Green } catch { Write-Host "❌ 下载失败:$($_.Exception.Message)" -ForegroundColor Red } finally { # 清理事件 Unregister-Event -SourceIdentifier DL -ErrorAction SilentlyContinue Unregister-Event -SourceIdentifier DLC -ErrorAction SilentlyContinue $wc.Dispose() } } function Expand-InPlace { # 1. 就地解压到与压缩包同级目录 # Expand-InPlace -Path C:\Temp\shim.zip # 2. 指定输出目录 # Expand-InPlace -Path C:\Temp\wix311-binaries.zip -Destination D:\Tools # 3. 覆盖已有 # Expand-InPlace -Path C:\Temp\wix311-binaries.zip -Force [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$Path, [string]$Destination, [switch]$Force, [switch]$Flatten # ← 新增开关,控制要不要带一层默认文件夹 ) begin { Add-Type -AssemblyName System.IO.Compression.FileSystem -EA SilentlyContinue } process { $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $root = if ($Destination) { $Destination } else { [IO.Path]::GetDirectoryName($Path) } $root = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($root) # 如果 -Flatten 指定,直接落在 root;否则再套一层压缩包名 $outFolder = if ($Flatten) { $root } else { $baseName = [IO.Path]::GetFileNameWithoutExtension($Path) if ($Path -match '\.tar\.(gz|bz2|xz)$') { $baseName = [IO.Path]::GetFileNameWithoutExtension($baseName) } Join-Path $root $baseName } if (-not (Test-Path $outFolder)) { New-Item -Path $outFolder -ItemType Directory -Force | Out-Null } if ($Force -and (Test-Path $outFolder)) { Remove-Item $outFolder -Recurse -Force; New-Item -Path $outFolder -ItemType Directory -Force | Out-Null } # 1) .zip if ($Path -match '\.zip$') { try { [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $outFolder) Write-Host "✅ 已解压:$outFolder" -ForegroundColor Green return } catch { Write-Warning "内置 ZipFile 解压失败,尝试 7z:$($_.Exception.Message)" $configPath = Join-Path $env:UserProfile ".popy" $downloadPath = Join-Path $configPath "tools" Write-Warning "如下载或解压失败,可手动下载压缩包放入 $downloadPath" } } # 2) 其余格式(7z / tar.gz / tar.bz2 / tar.xz / exe 自解压) # 先找 7z.exe,再找 7z $sevenZ = @( "${env:ProgramFiles}\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe" '7z' # PATH 里的 ) | Where-Object { $_ -and (Get-Command $_ -ErrorAction SilentlyContinue) } | Select-Object -First 1 if ($sevenZ) { $cmd = "& `"$sevenZ`" x `"$Path`" -o`"$outFolder`" -y" $null = Invoke-Expression $cmd if ($LASTEXITCODE -eq 0) { Write-Host "✅ 已解压:$outFolder" -ForegroundColor Green return } } Write-Error "无法解压 $Path,请确认格式或安装 7-Zip。" } } <# .SYNOPSIS 根据镜像列表尝试下载,失败给出离线说明 #> function Save-FileWithFallback { param( [string]$SavePath, [uri[]] $MirrorList, [string]$ExpectedHash, [ValidateSet('SHA256', 'SHA1', 'MD5')] [string]$HashAlgo = 'SHA256' ) # 本地已存在且哈希正确 => 直接返回 if (Test-Path $SavePath) { $localHash = (Get-FileHash $SavePath -Algorithm $HashAlgo).Hash if ($localHash -eq $ExpectedHash) { Write-Verbose "文件已存在且校验通过:$SavePath" return } Write-Verbose "本地文件哈希不符,将重新下载" } foreach ($url in $MirrorList) { try { Write-Host "正在从镜像下载:$url" Save-File -TargetURL $url -SavePath $SavePath -ErrorAction Stop $downloadedHash = (Get-FileHash $SavePath -Algorithm $HashAlgo).Hash if ($downloadedHash -eq $ExpectedHash) { Write-Verbose "下载完成且校验通过" return } Write-Warning "下载成功但哈希不符,尝试下一个镜像" } catch { Write-Verbose "镜像 $url 失败:$($_.Exception.Message)" } } # 全部失败 => 抛出友好信息 throw @" 所有镜像均无法下载,请检查网络或代理设置。 你也可以手动把文件放到: $SavePath 并确保 SHA256 值为: $ExpectedHash "@ } # ---- SystemPath.ps1 ---- # param( # [Parameter(Mandatory)] # [string]$ShimDir # ) # $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() # $principal = New-Object System.Security.Principal.WindowsPrincipal($identity) # if (-not($principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))) { # Write-Error "需要管理员权限" # exit 1 # } # Write-Host "正在写入系统PATH,请稍后" -ForegroundColor Cyan # # 1) 先清理用户级 PATH(如果存在) # $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') -split ';' # $userPath = $userPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' } # [Environment]::SetEnvironmentVariable('PATH', ($userPath -join ';'), 'User') # $userPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') -split ';' # $userPath = $userPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' } # [Environment]::SetEnvironmentVariable('PATH', ($userPath -join ';'), 'Machine') # # 2) 再写入系统级 PATH(置顶) # $sysPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') -split ';' # $sysPath = @($ShimDir) + ($sysPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' }) # [Environment]::SetEnvironmentVariable('PATH', ($sysPath -join ';'), 'Machine') # ---- Utils.ps1 ---- function Test-TakeOver { param( ) $configPath = Get-ConfigPath $shimsPath = Join-Path -Path $configPath -ChildPath 'shims' $shimPath = Join-Path $shimsPath "python.shim" if (-not (Test-Path $shimPath)) { return $false } $content = Get-Content -Path $shimPath -Raw if ($null -eq $content) { return $false } return $true } function Test-IsInit { param( ) $configPath = Get-ConfigPath $configFile = Join-Path $configPath 'config.json' if (-not (Test-Path $configFile)) { return $false } return $true } # ---- Activate.ps1 ---- function Invoke-Activate { param( [Parameter(Mandatory = $false)] [string]$Venv ) $storePath = Get-StorePath $venvsPath = Join-Path -Path $storePath -ChildPath "venvs" if (-not $Venv) { $executeVenvPath = Join-Path -Path $PWD.Path -ChildPath ".venv" } else { $executeVenvPath = Join-Path -Path $venvsPath -ChildPath $Venv } if (-not(Test-Path $executeVenvPath)) { Write-Host "没有指定的虚拟环境,请使用popy create创建" -ForegroundColor Red return } $specifiedToActivateVenvPath = Join-Path -Path $executeVenvPath -ChildPath "Scripts\activate.ps1" if (-not(Test-Path $specifiedToActivateVenvPath)) { Write-Host "虚拟环境可能没有正确的创建,请检查虚拟环境的文件结构,必要时可以重新创建" -ForegroundColor Red return } & $specifiedToActivateVenvPath if ($?) { Write-Host $envName "虚拟环境已激活" -ForegroundColor Cyan } else { Write-Host "激活失败" -ForegroundColor Red } } # ---- Add.ps1 ---- function Invoke-Add { param( [Parameter(Mandatory = $false)] [string]$Package, [Parameter(Mandatory = $false)] [switch]$Force ) if (-not $Package) { Write-Host "没有输入包名称或者requirements.txt" -ForegroundColor Red return } $isVirtualEnv = [bool]($env:VIRTUAL_ENV) if (-not $isVirtualEnv) { if (-not $Force) { Write-Host "当前不在虚拟环境中,请使用-f参数强制安装。" -ForegroundColor Yellow return } } # if (Test-Path $Package) { # pip install -r $Package -i https://pypi.tuna.tsinghua.edu.cn/simple # } else { # pip install -i https://pypi.tuna.tsinghua.edu.cn/simple $Package # } $cfg = Get-Configs $pipSource = $cfg.PipSources.($cfg.PipSource) if (Test-Path $Package) { $pipArgs = @("install", "-r", $Package, "-i", $pipSource) } else { $pipArgs = @("install", "-i", $pipSource, $Package) } # # $pipArgs = @("install", "-i", $tsinghuaMirror, $Package) # if (-not($args.Count -eq 0)) { # $pipArgs = $pipArgs + $args # } Write-Host "pip $pipArgs" -ForegroundColor Red Start-Process -FilePath "pip" -ArgumentList $pipArgs -NoNewWindow -Wait } # ---- Cd.ps1 ---- function Invoke-Cd { param( [Parameter(Mandatory = $false)] [string]$Path ) if (-not $Path) { Write-Host "需要指定你要去哪里" -ForegroundColor Red return } if ($Path -notin 'p', 'v', 'r') { Write-Host "函数内部的cd命令,只是带你快速到达versions,venvs以及requirements文件夹" -ForegroundColor Green Write-Host "popy cd p | popy cd v | popy cd r " -ForegroundColor Green return } $storePath = Get-StorePath $versionsPath = Join-Path -Path $storePath -ChildPath "versions" $venvsPath = Join-Path -Path $storePath -ChildPath "venvs" $requirementsPath = Join-Path -Path $storePath -ChildPath "requirements" New-Directory -checkedPath $versionsPath -description "versions path" New-Directory -checkedPath $venvsPath -description "venvs path" New-Directory -checkedPath $requirementsPath -description "requirements path" if ($Path -eq 'p') { if (Test-Path $versionsPath) { Set-Location -Path $versionsPath Write-Host "当前路径已切换到versions" -ForegroundColor Green } } if ($Path -eq 'v') { if (Test-Path $venvsPath) { Set-Location -Path $venvsPath Write-Host "当前路径已切换到venvs" -ForegroundColor Green } } if ($Path -eq 'r') { if (Test-Path $requirementsPath) { Set-Location -Path $requirementsPath Write-Host "当前路径已切换到requirements" -ForegroundColor Green } } } # ---- Create.ps1 ---- function Invoke-Create { param( [Parameter(Mandatory = $false)] [string]$venvName, [Parameter(Mandatory = $false)] [string]$Version ) $storePath = Get-StorePath $versionsPath = Join-Path -Path $storePath -ChildPath "versions" $venvsPath = Join-Path -Path $storePath -ChildPath "venvs" $configPath = Get-ConfigPath $shimsPath = Join-Path -Path $configPath -ChildPath 'shims' # $shimPath = Join-Path $shimsPath "python.shim" if ($venvName) { $executeVenvPath = Join-Path -Path $venvsPath -ChildPath $venvName if ($Version) { $pythonExePath = "$versionsPath\$Version\python.exe" if (-not(Test-Path $pythonExePath)) { Write-Host "并没有安装$Version 版本的python,自动下载安装" -ForegroundColor Red Invoke-Install $Version } } else { $isTakeOver = Test-TakeOver if (-not $isTakeOver) { Write-Host "popy 还没有接管您的python环境,请使用popy use 来设置您的python版本" Write-Host "或使用popy create -v 参数指定python版本" return } $pythonExePath = Join-Path $shimsPath "python.exe" } } else { $executeVenvPath = Join-Path -Path $PWD.Path -ChildPath ".venv" if ($Version) { $pythonExePath = "$versionsPath\$Version\python.exe" if (-not(Test-Path $pythonExePath)) { Write-Host "并没有安装$Version 版本的python,自动下载安装" -ForegroundColor Red Invoke-Install $Version } } else { $isTakeOver = Test-TakeOver if (-not $isTakeOver) { Write-Host "popy 还没有接管您的python环境,请使用popy use 来设置您的python版本" Write-Host "或使用popy create -v 参数指定python版本" return } $pythonExePath = Join-Path $shimsPath "python.exe" } } $versionname = & $pythonExePath -V 2>&1 # -V 输出版本,2>&1 把可能的 stderr 合并到 stdout $versionname = $versionname.Trim() # 去掉首尾空格/换行 if (Test-Path $executeVenvPath) { Write-Host "$executeVenvPath 虚拟环境已存在,手动删除后再重新创建" -ForegroundColor Red return } else { Write-Host "版本 $versionname 虚拟环境正在创建" -ForegroundColor Cyan & $pythonExePath -m venv $executeVenvPath if ($?) { Write-Host "$executeVenvPath 虚拟环境创建完成" -ForegroundColor Cyan } else { Write-Host "创建虚拟环境失败,请检查 Python 解释器路径或权限问题" -ForegroundColor Red } } } # ---- Deactivate.ps1 ---- function Invoke-Deactivate { param( ) if (Test-Path "function:deactivate") { deactivate Write-Host "虚拟环境已取消激活" } else { Write-Host "没有虚拟环需要取消激活" -ForegroundColor Red } } # ---- Freeze.ps1 ---- function Invoke-Freeze { param( [Parameter(Mandatory = $false)] [string]$TxtName, [Parameter(Mandatory = $false)] [switch]$Force ) if (-not $env:VIRTUAL_ENV) { Write-Host "当前并没有激活的虚拟环境,请激活虚拟环境再导出虚拟环境的包名" -ForegroundColor Red return } $storePath = Get-StorePath $requirementsPath = Join-Path -Path $storePath -ChildPath "requirements" New-Directory -checkedPath $requirementsPath -description "requirements path" if ($TxtName) { $TxtName = $TxtName -replace '\.txt$', '' # 先去掉可能多余的 .txt $requirementsFile = Join-Path -Path $requirementsPath -ChildPath "$TxtName.txt" if (Test-Path $requirementsFile) { if (-not $Force) { Write-Host "当前虚拟环境的依赖需求文件,如需覆盖请使用-f参数强制覆盖" -ForegroundColor Red return } } } else { $requirementsFile = Join-Path -Path $PWD.Path -ChildPath "requirements.txt" } pip freeze > $requirementsFile if ($?) { Write-Host "requirements 文件已导出到 $requirementsFile" -ForegroundColor Green } else { Write-Host "导出 requirements 文件失败。" -ForegroundColor Red } } # ---- GistPath.ps1 ---- function Invoke-Test { param( [Parameter(Mandatory = $false)] [string]$TxtName ) if (-not $env:VIRTUAL_ENV) { Write-Host "需要先激活一个虚拟环境。" -ForegroundColor Red return } if (-not $subcommand) { Write-Host "请输入一个路径" -ForegroundColor Red return } $currentVenvPath = $env:VIRTUAL_ENV $sitePackagesPath = Join-Path $currentVenvPath "Lib\site-packages" $pthFileName = "zzz_gistpath.pth" $filePath = Join-Path $sitePackagesPath $pthFileName $fileContent = $subcommand if (-not (Test-Path -Path $sitePackagesPath)) { Write-Host "'Lib\site-packages' 文件夹在当前虚拟环境中并不存在,可以选择手动创建" -ForegroundColor Yellow return } if (-not (Test-Path -Path $filePath)) { try { $fileContent | Set-Content -Path $filePath -Force Write-Host "文件 '$pthFileName' 已经创建在: $filePath" -ForegroundColor Green Write-Host "路径 '$fileContent' 已写入: $filePath" -ForegroundColor Green } catch { Write-Host "创建写入文件失败. Error: $_" -ForegroundColor Red } } else { try { $fileContent | Add-Content -Path $filePath -Force Write-Host "路径'$fileContent'已经添加到 '$pthFileName'." -ForegroundColor Green } catch { Write-Host "路径添加失败. Error: $_" -ForegroundColor Red } } } # ---- Help.ps1 ---- <# .SYNOPSIS popy – 极简 PowerShell Python 版本管理器 .DESCRIPTION popy 让你在同一台机器上轻松安装、切换、创建和删除多个 Python 解释器与虚拟环境, 并支持通过“需求文件”在不同项目之间快速重建一致的依赖环境。 所有数据默认存放在 $HOME\.popy 下,可通过环境变量 $POPPY_HOME 调整。 .PARAMETER Command 要执行的主命令(init / install / use / create / activate …)。 .PARAMETER Rest 命令后的所有剩余参数。 .PARAMETER Force -f 强制覆盖已存在的文件或目录。 .PARAMETER Version -v 指定 Python 版本(仅部分命令支持)。 .PARAMETER System -s 将 python/python3 等全局命令注册到系统 PATH(需要管理员权限)。 .EXAMPLE popy init 初始化 popy 目录结构并下载版本索引。 popy install 3.11.4 下载并安装 CPython 3.11.4。 popy use 3.11.4 把 3.11.4 设为当前全局默认解释器(写入 PATH)。 popy create myproj 在当前目录创建名为 myproj 的虚拟环境(默认使用当前全局解释器)。 popy create myproj -v 3.10.12 使用 3.10.12 创建虚拟环境。 popy activate myproj 激活虚拟环境 myproj(支持 PowerShell/Command Prompt)。 popy deactivate 退出当前虚拟环境。 popy freeze myproj 将虚拟环境 myproj 的依赖锁定为 requirements.txt。 popy requirements myproj 根据当前目录下的 requirements.txt 重建虚拟环境 myproj 的依赖。 popy cd v 直接 cd 到当前虚拟环境所在目录。 .LINK https://github.com #> function Invoke-Help { Write-Host @" init 初始化软件 update 更新可安装python版本列表 install <version> 安装指定版本python sources <source_name=source_url> 查看设置python安装包所有下载源 pipsources <source_name=source_url> 查看设置pip所有下载源 source <sourcename> 查看设置当前安装包下载源 pipsource <sourcename> 查看设置当前pip下载源 use|global|g <version/venvname> 查看设置当前全局python环境 pythons|ps 查看当前popy安装的所有python版本 create|c <venvname> <-v version> 创建虚拟环境,默认使用当前python全局版本,可以指定一个popy已安装的版本 activate|a <venvname> 激活一个虚拟环境,无参数时尝试查找项目内的.venv虚拟环境 deactivate|d 取消激活当前虚拟环境 venvs|vs 查看所有popy管理的虚拟环境名称 removevenv|rv <venvname> 删除一个虚拟环境,无参数时,尝试删除项目内的.venv虚拟环境 requirements|rs popy导出的requirements文件的名称 freeze|rf <file_name> 导出requirements文件到当前目录,如给定文件名则保存到popystore文件夹 removerequirement|rr <file_name> 删除一个venvs内的requirements文件 cd p|v|r 切换到python版本路径,虚拟环境路径,requirements路径 "@ -ForegroundColor Cyan } # gistpath <path> 给虚拟环境添加一个外部依赖库的位置,即给虚拟环境添加一个pth文件,以方便从系统中的一个指定位置import已有的代码 # ---- Init.ps1 ---- function Invoke-Init { param( # [Parameter(Mandatory = $false)] # [switch]$System, [Parameter(Mandatory = $false)] [switch]$Force ) $popyConfigPath = Get-ConfigPath New-Directory -checkedPath $popyConfigPath -description "config path" $configFile = Join-Path $popyConfigPath 'config.json' # 判断是否已存在 if (Test-Path $configFile) { if (-not $Force) { Write-Host "已存在初始化文件,无需初始化,如确定需要重置软件,请加-f参数" -ForegroundColor Yellow return } else { $choice = Read-Host "当前环境曾始化过,确定要重新初始化吗?(y/n)" if ($choice -notmatch '^(y|yes)$') { Write-Host "已取消初始化操作。" -ForegroundColor Yellow return } } } $cfg = Get-Configs $defaultStore = Join-Path $popyConfigPath 'PopyStore' do { $pathInput = Read-Host "请输入仓库路径(python安装文件、全局虚拟环境等)的存放位置,直接回车仓库将创建在用户目录 $popyConfigPath 下" # 1. 直接回车 → 用默认 if ([string]::IsNullOrWhiteSpace($pathInput)) { $storePath = $defaultStore break } # 2. 必须是绝对路径 if (-not [System.IO.Path]::IsPathRooted($pathInput)) { Write-Host "请输入绝对路径(如 C:\MyFolder)!" continue } # 3. 如果该路径已存在,则必须是个目录 if (Test-Path -LiteralPath $pathInput -PathType Leaf) { Write-Host "输入的路径是一个文件路径,而不是目录路径。请重新输入!" -ForegroundColor Red continue } # 4. 通过校验,拼最终仓库目录 $storePath = Join-Path $pathInput 'PopyStore' break } while ($true) # 5. 检测旧仓库提示 if ($null -ne $cfg -and $storePath -ne $cfg.storePath) { Write-Host "仓库目录已改变,如确定不再使用请手动删除 $($cfg.storePath)" } New-Directory -checkedPath $storePath -description "仓库文件夹" # 生成默认配置 $defaultConfig = @{ storePath = $storePath DownloadSource = 'huawei' DownloadSources = @{ default = 'https://www.python.org/ftp/python/' huawei = 'https://mirrors.huaweicloud.com/python/' } PipSource = 'tuna' PipSources = @{ default = 'https://pypi.org/simple' tuna = 'https://pypi.tuna.tsinghua.edu.cn/simple' ali = 'https://mirrors.aliyun.com/pypi/simple/' tencent = 'https://mirrors.cloud.tencent.com/pypi/simple' } versionName = 'system' versionType = 'version' } | ConvertTo-Json -Depth 3 # 写文件 try { $defaultConfig | Out-File -FilePath $configFile -Encoding utf8 -Force Write-Host "config.json 已生成/覆盖:$configFile" -ForegroundColor Green } catch { Write-Error "写入 config.json 失败:$_" # return } ################### $ShimDir = Join-Path $popyConfigPath 'shims' $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) # 1) 当前已是管理员 → 直接写系统 PATH if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "当前是管理员权限,正在写入系统PATH,请稍后" -ForegroundColor Cyan # 1) 先清理用户级 PATH(如果存在) $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') -split ';' $userPath = $userPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' } [Environment]::SetEnvironmentVariable('PATH', ($userPath -join ';'), 'User') $sysPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') -split ';' $sysPath = $sysPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' } [Environment]::SetEnvironmentVariable('PATH', ($sysPath -join ';'), 'Machine') # 2) 再写入系统级 PATH(置顶) $sysPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') -split ';' $sysPath = @($ShimDir) + ($sysPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' }) [Environment]::SetEnvironmentVariable('PATH', ($sysPath -join ';'), 'Machine') } # 2) 不是管理员 → 提权重跑当前脚本 else { Write-Host "当前不是管理员权限,正在写入用户PATH,请稍后" -ForegroundColor Cyan $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') -split ';' $userPath = $userPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' } [Environment]::SetEnvironmentVariable('PATH', ($userPath -join ';'), 'User') $ShimDir = Join-Path $popyConfigPath 'shims' $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') -split ';' $userPath = @($ShimDir) + ($userPath | Where-Object { $_ -notmatch '\\.popy\\shims\\?$' }) [Environment]::SetEnvironmentVariable('PATH', ($userPath -join ';'), 'User') Write-Host "已把 $ShimDir 加到用户 PATH 最前面" -ForegroundColor Green Write-Host "因为系统调用顺序,如果系统全局安装过python,只是添加到用户PATH可能会影响实际使用效果。如本机无其他全局安装的python可以忽略。" -ForegroundColor Green Write-Host "如需重新添加到系统PATH,用管理员权限重新运行 popy init -f" -ForegroundColor Green } ################## # $toolsPath = Join-Path $popyConfigPath 'tools' # New-Directory -checkedPath $toolsPath -description "tools path" # $wixDownloadUrl = "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip" # $wixZipFile = Join-Path $toolsPath 'wix311-binaries.zip' # $wixExpandPath = Join-Path $toolsPath 'wix' # $shimDownloadUrl = "https://github.com/ScoopInstaller/Shim/releases/download/v1.1.0/shim-1.1.0.zip" # $shimZipFile = Join-Path $toolsPath 'shim-1.1.0.zip' # $shimExpandPath = Join-Path $toolsPath 'shim' # . "$PSScriptRoot\..\lib\SaveFile.ps1" # try { # if (-not (Test-Path $wixZipFile)) { # Save-File -TargetURL $wixDownloadUrl -SavePath $wixZipFile # } # if (-not (Test-Path $shimZipFile)) { # Save-File -TargetURL $shimDownloadUrl -SavePath $shimZipFile # } # Write-Host "准备解压" # Expand-InPlace -Path $wixZipFile -Destination $wixExpandPath -Flatten -Force # Expand-InPlace -Path $shimZipFile -Destination $shimExpandPath -Flatten -Force # } # catch { # Write-Host "步骤失败,已终止后续操作:$($_.Exception.Message)" -ForegroundColor Red # # 不再往下走 # return # } # 1. 计算路径 $toolsPath = Join-Path $popyConfigPath 'tools' New-Directory -checkedPath $toolsPath -description "tools path" # 计算哈希 Get-FileHash C:\Temp\wix311-binaries.zip -Algorithm SHA256 $wixZipFile = Join-Path $toolsPath 'wix311-binaries.zip' $wixExpand = Join-Path $toolsPath 'wix' $wixHash = '2C1888D5D1DBA377FC7FA14444CF556963747FF9A0A289A3599CF09DA03B9E2E' # 官方 SHA256 $shimZipFile = Join-Path $toolsPath 'shim-1.1.0.zip' $shimExpand = Join-Path $toolsPath 'shim' $shimHash = 'C8452B3C4B8C219EDEF150CC423B0C844CB2D46381266011F6F076301E7E65D9' # 官方 SHA256 # 2. 构造镜像列表(可随用户环境变量追加) $wixMirrors = @( 'https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip' 'https://ghproxy.net/https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip' 'https://ghfast.top/https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip' ) $shimMirrors = @( 'https://github.com/ScoopInstaller/Shim/releases/download/v1.1.0/shim-1.1.0.zip' 'https://ghproxy.net/https://github.com/ScoopInstaller/Shim/releases/download/v1.1.0/shim-1.1.0.zip' 'https://ghfast.top/https://github.com/ScoopInstaller/Shim/releases/download/v1.1.0/shim-1.1.0.zip' ) # 3. 下载阶段 try { Save-FileWithFallback -SavePath $wixZipFile -MirrorList $wixMirrors -ExpectedHash $wixHash Save-FileWithFallback -SavePath $shimZipFile -MirrorList $shimMirrors -ExpectedHash $shimHash } catch { Write-Host $_.Exception.Message -ForegroundColor Red return # 提前终止 } # 4. 解压阶段(幂等,Force 覆盖即可) try { Expand-InPlace -Path $wixZipFile -Destination $wixExpand -Flatten -Force Expand-InPlace -Path $shimZipFile -Destination $shimExpand -Flatten -Force } catch { Write-Host "解压失败:$($_.Exception.Message)" -ForegroundColor Red return } } # ---- Install.ps1 ---- # param( # [Parameter(Mandatory = $true, Position = 0)] # [string]$Version # ) # Write-Host "这是 install 功能,目标版本:$Version" function Invoke-Install { param( [Parameter(Mandatory)] [string]$Version, [Parameter(Mandatory = $false)] [switch]$Force ) $cfg = Get-Configs if ($Version -eq "list") { $versions = $cfg.versions if ($null -eq $versions -or $versions.Count -eq 0) { Write-Host "没有任何版本信息,请使用popy update命令获取最新的可安装列表." } else { # 打印所有版本号 Write-Host "可用版本:" foreach ($version in $versions) { Write-Host $version } } return } $requestedVersion = $Version $availableVersions = $cfg.versions if ($null -eq $availableVersions -or $availableVersions.Count -eq 0) { Write-Host "没有任何版本信息,请使用popy update命令获取最新的可安装列表." return } $url = $cfg.DownloadSources.($cfg.DownloadSource) # ### $storePath = Get-StorePath $downloadPath = Join-Path -Path $storePath -ChildPath "downloads" New-Directory -checkedPath $downloadPath -description "下载目录" $versionsPath = Join-Path -Path $storePath -ChildPath "versions" New-Directory -checkedPath $versionsPath -description "versions" $versionPath = Join-Path -Path $versionsPath -ChildPath $requestedVersion $installerPath = "$downloadPath\python-$requestedVersion-amd64.exe" $downloadUrl = "$url$requestedVersion/python-$requestedVersion-amd64.exe" if ($availableVersions -contains $requestedVersion) { if (-not (Test-Path -Path $installerPath)) { Write-Host "正在下载并安装 Python $requestedVersion..." Save-File -TargetURL $downloadUrl -SavePath $installerPath } else { if ($Force) { Remove-Item -Path $installerPath -Force if (Test-Path -Path $installerPath) { Write-Host "删除失败" -ForegroundColor Red return } Write-Host "$installerPath 已删除" Write-Host "正在重新下载并安装 Python $requestedVersion..." Save-File -TargetURL $downloadUrl -SavePath $installerPath } else { Write-Host "$installerPath 已存在,可以直接安装,如解压错误,可以添加参数-f强制重新下载" } } if (-not(Test-Path $installerPath)) { Write-Host "下载出错,请确认版本,或者您所下载的版本$requestedVersion,没有适合当前系统环境的安装包,请尝试降小版本,或安装其他版本" -ForegroundColor Red return } #####提取安装 Write-Host "Python $requestedVersion 正在解压!" -ForegroundColor Green $extractPath = Join-Path -Path $downloadPath -ChildPath $requestedVersion $toolPath = Get-ToolsPath $strDirWiX = Join-Path -Path $toolPath -ChildPath 'wix' $darkExe = Join-Path -Path $strDirWiX -ChildPath 'dark.exe' New-Directory -checkedPath $extractPath -description "msi目录" $extractCommand = Start-Process -FilePath $darkExe -ArgumentList "-x", "`"$extractPath`"", "`"$installerPath`"" -Wait -PassThru -WindowStyle Hidden # 如果还弹窗 可以使用$command = "$darkExe -x `"$cachePath`" `"$installFile`"" #Invoke-Expression $command if ($extractCommand.ExitCode -ne 0) { Write-Error "Error extracting the embedded portion from the installer." return -1 } $extractMsiPath = Join-Path -Path $extractPath -ChildPath "AttachedContainer\*.msi" Get-ChildItem -Path $extractMsiPath | Move-Item -Destination $extractPath -Force Get-ChildItem -Path $extractPath -File | ForEach-Object { if ($_.Extension -ne ".msi" -or $_.BaseName -in @("appendpath", "launcher", "path", "pip")) { Remove-Item -Path $_.FullName -Force } } Get-ChildItem -Path $extractPath -Directory | ForEach-Object { Remove-Item -Path $_.FullName -Force -Recurse } New-Directory -checkedPath $versionPath -description "版本目录" $installJobs = @() Get-ChildItem -Path $extractPath -Filter *.msi | ForEach-Object { $msiFilePath = $_.FullName $job = Start-Job -ScriptBlock { param($msiFilePath, $versionPath) Start-Process -FilePath "msiexec" -ArgumentList "/quiet", "/a", "`"$msiFilePath`"", "TARGETDIR=`"$versionPath`"" -NoNewWindow -Wait if ($LASTEXITCODE -ne 0) { Write-Error "Error installing $($_.BaseName) component MSI. Exit code: $LASTEXITCODE" } } -ArgumentList $msiFilePath, $versionPath $installJobs += $job } Write-Host "Python $requestedVersion 正在安装!" -ForegroundColor Green # 等待所有安装任务完成 $total = $installJobs.Count $completed = 0 while ($completed -lt $total) { $running = Get-Job -State Running $completed = $total - $running.Count $percent = [int]($completed / $total * 100) Write-Progress -Activity "Installing packages" ` -Status "$completed / $total finished" ` -PercentComplete $percent Start-Sleep -Milliseconds 300 # 避免 CPU 空转 } # 清理作业 $installJobs | Remove-Job -Force # 删除安装路径中的重复 MSI 文件 Get-ChildItem -Path $extractPath -Filter *.msi | ForEach-Object { $msiPath = Join-Path -Path $versionPath -ChildPath $_.Name if (Test-Path -Path $msiPath) { Remove-Item -Path $msiPath -Force } } $ensurepipPath = Join-Path -Path $versionPath -ChildPath "Lib\ensurepip" if (Test-Path -Path $ensurepipPath) { # Write-Host "pip 正在安装!" -ForegroundColor Green $pythonExe = Join-Path -Path $versionPath -ChildPath "python.exe" & $pythonExe -E -s -m ensurepip -U --default-pip 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "Error installing pip." return -1 } } ##### Write-Host "Python $requestedVersion 安装完成!" -ForegroundColor Green } } # ---- PipSource.ps1 ---- function Invoke-PipSource { param( [Parameter(Mandatory = $false)] [string]$pipSource ) $cfg = Get-Configs if (-not $pipSource) { $pipSource = $cfg.PipSource Write-Host "当前python安装源为 $pipSource" return } else { $exists = $cfg.PipSources.PSObject.Properties.Name -contains $pipSource if (-not $exists) { Write-Host "运行popy pipsources查看所有可用源,popy pipsource 设置的源必须是在pipsources内的" -ForegroundColor Red return } Set-Config -Key 'PipSource' -Value $pipSource $cfg = Get-Configs $pipSource = $cfg.PipSource Write-Host "python安装源切换为 $pipSource" } } # ---- PipSources.ps1 ---- function Invoke-PipSources { param( [Parameter(Mandatory = $false)] [string]$pipSourceKeyValue ) $cfg = Get-Configs if (-not $pipSourceKeyValue) { $pipSources = $cfg.PipSources if ($null -eq $pipSources -or $pipSources.Count -eq 0) { Write-Host "没有任何安装源,请重新init,或者手动设置安装源如:popy pipsources default=https://pypi.org/simple" } else { # 打印所有版本号 Write-Host "可用安装源:" foreach ($name in $pipSources.PSObject.Properties.Name) { $url = $pipSources.$name Write-Host "$name=$url" } } return } else { if ($pipSourceKeyValue -match "^(.+?)=(.+)$") { $key = $matches[1].Trim() $value = $matches[2].Trim() # 检查是否已存在键,然后更新或添加 $old = $cfg.PipSources $ht = [ordered]@{} $old.PSObject.Properties.Name | Sort-Object -Descending | ForEach-Object { $ht[$_] = $old.$_ } # 2. 新增键自动排在最后 $ht[$key] = $value # 3. 再转回 PSCustomObject $sourcesUpdated = [PSCustomObject]$ht Set-Config -Key 'PipSources' -Value $sourcesUpdated $cfg = Get-Configs $pipSources = $cfg.PipSources Write-Host "可用安装源:" foreach ($name in $pipSources.PSObject.Properties.Name) { $url = $pipSources.$name Write-Host "$name=$url" } } else { Write-Host "无效的子命令格式。请使用 'pipsources <key>=<value>'。" -ForegroundColor Red } } } # ---- RemoveRequirement.ps1 ---- function Invoke-RemoveRequirement { param( [Parameter(Mandatory = $false)] [string]$TxtName ) $storePath = Get-StorePath $requirementsPath = Join-Path -Path $storePath -ChildPath "requirements" New-Directory -checkedPath $requirementsPath -description "requirements path" if ($TxtName) { $TxtName = $TxtName -replace '\.txt$', '' # 先去掉可能多余的 .txt $requirementsFile = Join-Path -Path $requirementsPath -ChildPath "$TxtName.txt" } else { $requirementsFile = Join-Path -Path $PWD.Path -ChildPath "requirements.txt" } if (Test-Path $requirementsFile) { # $confirm = Read-Host "确定要删除$filename?(Y/N)" # if ($confirm -ne 'Y' -and $confirm -ne 'y') { # Write-Host "删除操作已取消!" -ForegroundColor Yellow # return # } Remove-Item -Path $requirementsFile -Force if ($?) { Write-Host "$requirementsFile 依赖包文件列表删除完毕。" -ForegroundColor Green } else { Write-Host "依赖包文件列表删除失败。" -ForegroundColor Red } } else { Write-Host "$TxtName.txt不存,在无需删除" -ForegroundColor Yellow Write-Host "使用 popy rs 查看所有存档的 requirements.txt 文件" -ForegroundColor Green } } # ---- RemoveVenv.ps1 ---- function Invoke-RemoveVenv { param( [Parameter(Mandatory = $false)] [string]$Venv ) $storePath = Get-StorePath $venvsPath = Join-Path -Path $storePath -ChildPath "venvs" if (-not $Venv) { $executeVenvPath = Join-Path -Path $PWD.Path -ChildPath ".venv" } else { $executeVenvPath = Join-Path -Path $venvsPath -ChildPath $Venv } if (Test-Path $executeVenvPath) { if ($env:VIRTUAL_ENV -eq $executeVenvPath) { Write-Host "当前激活的虚拟环境与要删除的虚拟环境相同,先停用再删除。" -ForegroundColor Yellow if (Test-Path "function:deactivate") { deactivate Write-Host "虚拟环境已停用。" -ForegroundColor Green } else { Write-Host "无法停用虚拟环境,请检查环境变量或手动停用。" -ForegroundColor Red return } } $confirm = Read-Host "确定要删除虚拟环境(Y/N)" if ($confirm -ne 'Y' -and $confirm -ne 'y') { Write-Host "删除操作已取消!" -ForegroundColor Yellow return } Remove-Item -Path $executeVenvPath -Recurse -Force if ($?) { Write-Host "$executeVenvPath 虚拟环境已成功删除。" -ForegroundColor Green } else { Write-Host "删除虚拟环境失败。" -ForegroundColor Red } } else { Write-Host "$subcommand 虚拟环境不存在,无需删除。" -ForegroundColor Yellow } } # ---- Requirements.ps1 ---- function Invoke-Requirements { param( [Parameter(Mandatory = $false)] [string]$Pattern ) $storePath = Get-StorePath $requirementsPath = Join-Path -Path $storePath -ChildPath "requirements" New-Directory -checkedPath $requirementsPath -description "requirements path" $requirementsFiles = Get-ChildItem -Path $requirementsPath -Filter "*.txt" if ($requirementsFiles.Count -eq 0) { Write-Host "当前没有任何已导出的requirement.txt文件" -ForegroundColor Red return } if ($Pattern) { $filteredRequirements = $requirementsFiles | Where-Object { $_.Name -like "*$Pattern*" } if ($filteredRequirements.Count -eq 0) { Write-Host "没有找到匹配$Pattern 的requirement.txt文件" -ForegroundColor Yellow } else { $filteredRequirements | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } } } else { $requirementsFiles | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } Write-Host "通过添加额外的参数可以过滤所带有需要的字样的结果,如popy rs main 就会查找所有带有main字样的文件" -ForegroundColor Yellow } } # ---- Source.ps1 ---- function Invoke-Source { param( [Parameter(Mandatory = $false)] [string]$source ) $cfg = Get-Configs if (-not $source) { $downloadSource = $cfg.DownloadSource Write-Host "当前python安装源为 $downloadSource" return } else { $exists = $cfg.DownloadSources.PSObject.Properties.Name -contains $source if (-not $exists) { Write-Host "运行popy sources查看所有可用源,popy source 设置的源必须是在sources内的" -ForegroundColor Red return } Set-Config -Key 'DownloadSource' -Value $source $cfg = Get-Configs $downloadSource = $cfg.DownloadSource Write-Host "python安装源切换为 $downloadSource" } } # ---- Sources.ps1 ---- function Invoke-Sources { param( [Parameter(Mandatory = $false)] [string]$sourceKeyValue ) $cfg = Get-Configs if (-not $sourceKeyValue) { $sources = $cfg.DownloadSources if ($null -eq $sources -or $sources.Count -eq 0) { Write-Host "没有任何安装源,请重新下载,或者手动设置安装源如:popy sources default=https://www.python.org/ftp/python/" } else { # 打印所有版本号 Write-Host "可用安装源:" foreach ($name in $sources.PSObject.Properties.Name) { $url = $sources.$name Write-Host "$name=$url" } } return } else { if ($sourceKeyValue -match "^(.+?)=(.+)$") { $key = $matches[1].Trim() $value = $matches[2].Trim() # 检查是否已存在键,然后更新或添加 $old = $cfg.DownloadSources $ht = [ordered]@{} $old.PSObject.Properties.Name | Sort-Object -Descending | ForEach-Object { $ht[$_] = $old.$_ } # 2. 新增键自动排在最后 $ht[$key] = $value # 3. 再转回 PSCustomObject $sourcesUpdated = [PSCustomObject]$ht Set-Config -Key 'DownloadSources' -Value $sourcesUpdated $cfg = Get-Configs $sources = $cfg.DownloadSources Write-Host "可用安装源:" foreach ($name in $sources.PSObject.Properties.Name) { $url = $sources.$name Write-Host "$name=$url" } } else { Write-Host "无效的子命令格式。请使用 'sources <key>=<value>'。" -ForegroundColor Red } } } # ---- test.ps1 ---- function Invoke-Test { param( [Parameter(Mandatory = $false)] [switch]$System, [Parameter(Mandatory = $false)] [switch]$Force ) } # ---- Update.ps1 ---- function Invoke-Update { param( # [Parameter(Mandatory = $false)] # [switch]$System, # [Parameter(Mandatory = $false)] # [switch]$Force ) $cfg = Get-Configs # $sourceName = $cfg.DownloadSource $url = $cfg.DownloadSources.($cfg.DownloadSource) Write-Host $url # 获取网页内容 if ($PSVersionTable.PSVersion.Major -le 5) { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } $response = Invoke-WebRequest -Uri $url -UseBasicParsing # Write-Host $response # 提取版本信息 $versions = $response.Links | Where-Object { $_.href -match '^\d+\.\d+(?:\.\d+)?/$' } | ForEach-Object { $_.href.TrimEnd('/') } | Where-Object { [version]$_ -ge [version]'3.7' } | Sort-Object { [version]$_ } -Descending if ($null -eq $versions -or $versions.Count -eq 0) { Write-Host "没有找到任何python版本,请检查网址是否正确." } # 打印版本列表到控制台 Write-Host "发现可用版本:" foreach ($version in $versions) { Write-Host $version } # # 保存版本列表到文件 Set-Config -Key 'versions' -Value $versions Write-Host "安装列表已保存到 $pythonVersionsFile" } # ---- Use.ps1 ---- function Invoke-Use { param( [Parameter(Mandatory = $false)] [string]$UseVersion ) $cfg = Get-Configs $versionType = $cfg.versionType $versionName = $cfg.versionName if (-not $UseVersion) { $isTakeOver = Test-TakeOver if (-not $isTakeOver) { Write-Host "popy 还没有接管您的python环境,请使用popy use <version|venv>来设置您的python版本" Write-Host "或使用popy create -v 参数指定python版本" return } $configPath = Get-ConfigPath $shimsPath = Join-Path -Path $configPath -ChildPath 'shims' $pythonExePath = Join-Path $shimsPath "python.exe" $version = & $pythonExePath -V 2>&1 # -V 输出版本,2>&1 把可能的 stderr 合并到 stdout $version = $version.Trim() # 去掉首尾空格/换行 if ($versionName -eq "system") { Write-Host "全局版本为系统默认版本" -ForegroundColor Green } elseif ($versionType -eq "version") { Write-Host "全局版本为versions $versionName ,python版本$version " -ForegroundColor Green } elseif ($versionType -eq "venv") { Write-Host "全局版本为venv $versionName ,python版本$version " -ForegroundColor Green } return } $configPath = Get-ConfigPath $toolsPath = Join-Path -Path $configPath -ChildPath 'tools' $scriptsVersionPath = Join-Path -Path $configPath -ChildPath 'shims' $binPath = Join-Path -Path $toolsPath -ChildPath 'shim' # 检查$scriptsVersionPath下有没有python.exe和python.shim # exe没有就从$binPath里复制一份过来 $exePath = Join-Path $scriptsVersionPath "python.exe" $shimPath = Join-Path $scriptsVersionPath "python.shim" $pipexePath = Join-Path $scriptsVersionPath "pip.exe" $pipshimPath = Join-Path $scriptsVersionPath "pip.shim" $storePath = Get-StorePath $versionsPath = Join-Path -Path $storePath -ChildPath "versions" $venvPath = Join-Path -Path $storePath -ChildPath "venvs" New-Directory -checkedPath $scriptsVersionPath -description "shims path" New-Directory -checkedPath $versionsPath -description "versions path" New-Directory -checkedPath $venvPath -description "venv path" if (-not (Test-Path $exePath)) { $sourceExe = Join-Path $binPath "shim.exe" if (Test-Path $sourceExe) { Copy-Item -Path $sourceExe -Destination $exePath -Force Write-Host "已复制 python.exe 到 $exePath" } else { Write-Warning "源文件不存在:$sourceExe ,请手动下载,或popy init自动下载" } } if (-not (Test-Path $shimPath)) { New-ShimFile $shimPath } if (-not (Test-Path $pipexePath)) { $sourceExe = Join-Path $binPath "shim.exe" if (Test-Path $sourceExe) { Copy-Item -Path $sourceExe -Destination $pipexePath -Force Write-Host "已复制 pip.exe 到 $pipexePath" } else { Write-Warning "源文件不存在:$sourceExe" } } if (-not (Test-Path $pipshimPath)) { New-ShimFile $pipshimPath } $wexePath = Join-Path $scriptsVersionPath "pythonw.exe" $wshimPath = Join-Path $scriptsVersionPath "pythonw.shim" if (-not (Test-Path $wexePath)) { $sourceExe = Join-Path $binPath "shim.exe" if (Test-Path $sourceExe) { Copy-Item -Path $sourceExe -Destination $wexePath -Force Write-Host "已复制 python.exe 到 $wexePath" } else { Write-Warning "源文件不存在:$sourceExe ,请手动下载,或popy init自动下载" } } if (-not (Test-Path $wshimPath)) { New-ShimFile $wshimPath } $requestedVersion = $UseVersion $globalVersionPath = Join-Path -Path $versionsPath -ChildPath $requestedVersion $globalVenvPath = Join-Path -Path $venvPath -ChildPath $requestedVersion if ($requestedVersion -eq "system") { $systemPython = (where.exe python) -split "`r`n" | Where-Object { $_ -notmatch '\\.popy\\shims\\python\.exe$' } | Select-Object -First 1 if ($systemPython) { # 写入 .shim 文件 $shimContent = "path = `"$systemPython`"" Set-Content -Path $shimPath -Value $shimContent $wshimContent = $shimContent -replace 'python\.exe', 'pythonw.exe' Set-Content -Path $wshimPath -Value $wshimContent Write-Host "全局版本已切换到系统 Python:$systemPython" -ForegroundColor Green $pipPath = Join-Path (Split-Path $systemPython -Parent) "Scripts\pip.exe" if (Test-Path $pipPath) { $pipShimContent = "path = `"$pipPath`"" Set-Content -Path $pipshimPath -Value $pipShimContent Write-Host "pip 路径已写入:$pipPath" -ForegroundColor Cyan } else { Write-Warning "未找到 pip.exe,跳过写入 pip 的 .shim 文件" } } Set-ConfigValue -FilePath $versionFile -Key "name" -Value "system" return } else { if (-not (Test-Path -Path $globalVersionPath)) { if (-not (Test-Path -Path $globalVenvPath)) { Write-Host "没有找到设置的python版本或者已经安装的虚拟环境" -ForegroundColor Red return } } } $checkedVersionPath = Join-Path -Path $versionsPath -ChildPath $requestedVersion if (Test-Path -Path $checkedVersionPath) { $pythonPath = Join-Path -Path $checkedVersionPath -ChildPath "python.exe" $pipPath = Join-Path -Path $checkedVersionPath -ChildPath "Scripts\pip.exe" $wpythonPath = Join-Path -Path $checkedVersionPath -ChildPath "pythonw.exe" if (Test-Path $pythonPath) { $pythonShimContent = "path = `"$pythonPath`"" Set-Content -Path $shimPath -Value $pythonShimContent Write-Host "python 路径已写入:$pythonPath" -ForegroundColor Cyan } if (Test-Path $pipPath) { $pipShimContent = "path = `"$pipPath`"" Set-Content -Path $pipshimPath -Value $pipShimContent Write-Host "pip 路径已写入:$pipPath" -ForegroundColor Cyan } if (Test-Path $wpythonPath) { $wshimContent = "path = `"$wpythonPath`"" Set-Content -Path $wshimPath -Value $wshimContent Write-Host "pythonw 路径已写入:$wpythonPath" -ForegroundColor Cyan } Set-Config -Key "versionType" -Value "version" Set-Config -Key "versionName" -Value $requestedVersion return } $checkedVenvPath = Join-Path -Path $venvPath -ChildPath $requestedVersion if (Test-Path -Path $checkedVenvPath) { $pythonPath = Join-Path -Path $checkedVenvPath -ChildPath "Scripts\python.exe" $pipPath = Join-Path -Path $checkedVenvPath -ChildPath "Scripts\pip.exe" $wpythonPath = Join-Path -Path $checkedVenvPath -ChildPath "Scripts\pythonw.exe" if (Test-Path $pythonPath) { $pythonShimContent = "path = `"$pythonPath`"" Set-Content -Path $shimPath -Value $pythonShimContent Write-Host "python 路径已写入:$pythonPath" -ForegroundColor Cyan } if (Test-Path $pipPath) { $pipShimContent = "path = `"$pipPath`"" Set-Content -Path $pipshimPath -Value $pipShimContent Write-Host "pip 路径已写入:$pipPath" -ForegroundColor Cyan } if (Test-Path $wpythonPath) { $wshimContent = "path = `"$wpythonPath`"" Set-Content -Path $wshimPath -Value $wshimContent Write-Host "pythonw 路径已写入:$wpythonPath" -ForegroundColor Cyan } Set-Config -Key "versionType" -Value "venv" Set-Config -Key "versionName" -Value $requestedVersion return } Write-Host "全局版本已切换到 $requestedVersion " -ForegroundColor Green } function New-ShimFile { param ( [Parameter(Mandatory)] [string]$shimPath ) if (-not (Test-Path $shimPath)) { New-Item -Path $shimPath -ItemType File -Force | Out-Null Write-Host "已创建空 shim 文件:$shimPath" } } # ---- Venvs.ps1 ---- function Invoke-Venvs { param( [Parameter(Mandatory = $false)] [string]$Pattern ) $storePath = Get-StorePath $venvsPath = Join-Path -Path $storePath -ChildPath "venvs" $venvList = Get-ChildItem -Path $venvsPath -Directory if ($venvList.Count -eq 0) { Write-Host "当前没有任何虚拟环境" -ForegroundColor Red return } if ($Pattern) { $filteredVenvs = $venvList | Where-Object { $_.Name -like "*$Pattern*" } if ($filteredVenvs.Count -eq 0) { Write-Host "没有找到匹配$Pattern 的虚拟环境" -ForegroundColor Yellow } else { $filteredVenvs | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } } } else { $venvList | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } Write-Host "通过第二个参数可以搜索带有某些字样的虚拟环境,如popy vs main 就会查找所有带有main字样的文件夹" -ForegroundColor Yellow } } # ---- Versions.ps1 ---- function Invoke-Versions { param( [Parameter(Mandatory = $false)] [string]$Pattern ) $storePath = Get-StorePath $versionsPath = Join-Path -Path $storePath -ChildPath "versions" New-Directory -checkedPath $versionsPath -description "versions" $versionsList = Get-ChildItem -Path $versionsPath -Directory if ($versionsList.Count -eq 0) { Write-Host "当前没有安装任何python版本" -ForegroundColor Red return } if ($Pattern) { $filteredVersions = $versionsList | Where-Object { $_.Name -like "*$Pattern*" } if ($filteredVersions.Count -eq 0) { Write-Host "没有找到匹配$Pattern 的Python版本" -ForegroundColor Yellow } else { $filteredVersions | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } } } else { $versionsList | ForEach-Object { Write-Host $_.Name -ForegroundColor Cyan } Write-Host "通过第二个参数可以搜索带有某些字样的虚拟环境,如popy ps 3.9 就会查找所有带有3.9字样的文件夹" -ForegroundColor Yellow } } function popy { param( [Parameter(Mandatory = $false, Position = 0)] [string]$Command, [Parameter(ValueFromRemainingArguments)] [string[]]$Rest, [Parameter(Mandatory = $false)] [Alias('f')] [switch]$Force, [Parameter(Mandatory = $false)] [Alias('v')] [string]$Version # [Parameter(Mandatory = $false)] # [switch]$System # [Parameter(Mandatory = $false)] # [string]$NowPath ) $isInit = Test-IsInit if ((-not $isInit) -and -not ($Command -eq 'init')) { Write-Host "使用前请先使用popy init 来初始化程序,做基础配置和依赖下载" -ForegroundColor Red return } $ForceTable = @{ Force = $Force.IsPresent } # $SystemTable = @{ System = $System.IsPresent } switch ($Command.ToLower()) { 'init' { Invoke-Init @ForceTable } { 'install', 'i' -contains $_ } { if ($Rest.Count -eq 0) { Write-Host '缺少参数:popy install <version>' -ForegroundColor Red return } # 按需加载 Invoke-Install $Rest[0] @ForceTable } 'update' { Invoke-Update } 'sources' { if ($Rest.Count -eq 0) { Invoke-Sources } else { Invoke-Sources $Rest[0] } } 'source' { if ($Rest.Count -eq 0) { Invoke-Source } else { Invoke-Source $Rest[0] } } 'pipsources' { if ($Rest.Count -eq 0) { Invoke-PipSources } else { Invoke-PipSources $Rest[0] } } 'pipsource' { if ($Rest.Count -eq 0) { Invoke-PipSource } else { Invoke-PipSource $Rest[0] } } { 'use', 'u', 'global', 'g' -contains $_ } { if ($Rest.Count -eq 0) { # Write-Host '缺少参数:popy use <version>' -ForegroundColor Red # return Invoke-Use } else { Invoke-Use $Rest[0] } } { 'create', 'c', 'venv', 'v' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Create -Version $Version } else { Invoke-Create $Rest[0] -Version $Version } } { 'versions', 'ps' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Versions } else { Invoke-Versions $Rest[0] } } { 'venvs', 'vs' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Venvs } else { Invoke-Venvs $Rest[0] } } { 'requirements', 'rs' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Requirements } else { Invoke-Requirements $Rest[0] } } { 'activate', 'a' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Activate } else { Invoke-Activate $Rest[0] } } { 'deactivate', 'd' -contains $_ } { Invoke-Deactivate } { 'removevenv', 'rv' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-RemoveVenv } else { Invoke-RemoveVenv $Rest[0] } } { 'freeze', 'f' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-Freeze } else { Invoke-Freeze $Rest[0] @ForceTable } } { 'removerequirement', 'rr' -contains $_ } { if ($Rest.Count -eq 0) { Invoke-RemoveRequirement } else { Invoke-RemoveRequirement $Rest[0] } } 'cd' { if ($Rest.Count -eq 0) { Write-Host '缺少参数:popy cd <p|v|r>' -ForegroundColor Red return } Invoke-Cd $Rest[0] } 'add' { if ($Rest.Count -eq 0) { Write-Host '缺少参数:popy add <package|requirements.txt>' -ForegroundColor Red return } Invoke-Add $Rest[0] @ForceTable } default { Invoke-Help } } } Export-ModuleMember -Function popy |