From 94a6951597f9cbdf7f1e4fcbd69160a19cb27529 Mon Sep 17 00:00:00 2001 From: dongly Date: Sun, 1 Feb 2026 01:07:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B4=E5=90=88=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构了安装脚本 - 将安装脚本统一移动到 tools 目录 - 更新 README 文档 - 支持中/英双语 - Linux/MacOS 统一使用虚拟环境 #257 #269 - 可能自定义安装目录(默认: ~/.rt-env) #230 - 根据 IP,决定是否使用国内镜像(软件/仓库/PyPI) - 多平台共用的操作由 touch_env.py 统一处理,方便维护 - Linux / MacOS 整合为一个安装文件 install.sh - 可选是否安装 pyocd - 对已经安装 ENV, 可选择多种保留策略(主要是因工具链下载时间太长了) - 激活后显示欢迎词 - 可设定 env、package 、sdk 仓库的地址及分支,方便测试这些仓库的更改 - Windows 安装脚本: - 设置 ps 脚本的执行策略 - 自动搜索系统中的 Python - 或自动下载并安装便携式 Python - 为 Python 设置长路径支持 - 自动下载安装 Git 最新版 - 使用了 AI 辅助 已测试平台: - Windows 11: Windows PowerShell 5.1 - Windows 11: PowerShell 7.5 - Windows WSL2: Ubuntu 24.04.3 LTS --- .gitattributes | 3 +- README.md | 425 ++++++++- README_ZH.md | 417 ++++++++ env.ps1 | 45 +- env.py | 55 +- env.sh | 25 +- install_arch.sh | 70 -- install_macos.sh | 76 -- install_suse.sh | 17 - install_ubuntu.sh | 143 ++- install_windows.ps1 | 131 --- pyproject.toml | 31 +- setup.py | 55 +- tools/install.ps1 | 2227 +++++++++++++++++++++++++++++++++++++++++++ tools/install.sh | 752 +++++++++++++++ tools/touch_env.py | 1901 ++++++++++++++++++++++++++++++++++++ touch_env.ps1 | 35 - touch_env.py | 0 touch_env.sh | 33 - 19 files changed, 5938 insertions(+), 503 deletions(-) create mode 100644 README_ZH.md delete mode 100755 install_arch.sh delete mode 100755 install_macos.sh delete mode 100755 install_suse.sh delete mode 100644 install_windows.ps1 create mode 100644 tools/install.ps1 create mode 100755 tools/install.sh create mode 100644 tools/touch_env.py delete mode 100644 touch_env.ps1 delete mode 100644 touch_env.py delete mode 100755 touch_env.sh diff --git a/.gitattributes b/.gitattributes index 13da7fbe..4e978fc5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -29,4 +29,5 @@ *.md text eol=crlf *.bat text eol=crlf -*.ps1 text eol=crlf \ No newline at end of file +*.ps1 text eol=crlf encoding=UTF-8-BOM +*.sh text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 43a466b4..be35e993 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,417 @@ +
+ # Python Scripts for RT-Thread Env -> WARNING -> -> [env v2.0](https://github.com/RT-Thread/env/tree/master) and [env-windows v2.0](https://github.com/RT-Thread/env-windows/tree/v2.0.0) only **FULL SUPPORT** RT-Thread > v5.1.0 or [master](https://github.com/rt-thread/rt-thread) branch. if you work on RT-Thread <= v5.1.0, please use [env v1.5.x](https://github.com/RT-Thread/env/tree/v1.5.x) for linux, [env-windows v1.5.x](https://github.com/RT-Thread/env-windows/tree/v1.5.2) for windows +[![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) +[![Python](https://img.shields.io/badge/Python-3.6+-green.svg)](https://www.python.org/) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]() + +**[简体中文](README_ZH.md) | English** + +
+ +--- + +## ⚠️ Version Compatibility Notice + +| RT-Thread Version | Recommended Env Version | +|-------------------|------------------------| +| **> v5.1.0** or **master branch** | ✅ **env v2.0** (current version) | +| **≤ v5.1.0** | ⚠️ [env v1.5.x](https://github.com/RT-Thread/env/tree/v1.5.x) (Linux) / [env-windows v1.5.2](https://github.com/RT-Thread/env-windows/tree/v1.5.2) (Windows) | + +### 🔄 Key Changes in v2.0 + +- ✨ **Python Version Upgrade**: Upgraded from Python 2 to Python 3 +- 🔧 **Configuration System Refactor**: Replaced kconfig-frontends with Python kconfiglib +- 📦 **Dependency Management Optimization**: Installer automatically handles kconfiglib dependency + +> **Note**: env v2.0 is incompatible with kconfiglib from env v1.5.x. If you need to switch versions, please run `pip uninstall kconfiglib` first. + +--- + +## 📚 Table of Contents + +- [🚀 Quick Start](#-quick-start) + - [Windows Installation](#windows-installation) + - [Linux/macOS Installation](#linuxmacos-installation) +- [⚙️ Installation Script Parameters](#️-installation-script-parameters) +- [💡 Usage Guide](#-usage-guide) +- [❓ Troubleshooting](#-troubleshooting) +- [📖 Resources](#-resources) + +--- + +## 🚀 Quick Start + +### Windows Installation + +#### Prerequisites + +| Item | Requirements | +|------|-------------| +| **Privileges** | 📋 First installation requires **administrator privileges** (to set execution policy and long path support)
Subsequent updates can use normal user privileges | +| **PowerShell** | ✅ Windows PowerShell v5.1+
✅ PowerShell 7+ | + +#### Encoding Compatibility + +> ⚠️ **Important Notice** > -> env v2.0 has made the following important changes: -> - Upgrading Python version from v2 to v3 -> - Replacing kconfig-frontends with Python kconfiglib +> | PowerShell Version | Encoding Support | Notes | +> |-------------------|------------------|-------| +> | Windows PowerShell (v5.1) | ⚠️ GB2312 default, requires UTF-8 with BOM | Chinese systems require special handling | +> | PowerShell 7+ | ✅ Native UTF-8 | No encoding issues | > -> env v2.0 require python kconfiglib (install by `pip install kconfiglib`), but env v1.5.x confilt with kconfiglib (please run `pip uninstall kconfiglib`) +> Installation scripts are configured as UTF-8 with BOM encoding to ensure compatibility. + +#### Installation Commands + +
+🌐 International Users (Official Source) + +```powershell +Set-ExecutionPolicy -ExecutionPolicy Bypass Process; irm https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.ps1 | Out-File -Encoding utf8 .\install.ps1; .\install.ps1; Remove-Item .\install.ps1 +``` + +
+ +
+🇨🇳 Users in China (Mirror Source) + +```powershell +Set-ExecutionPolicy -ExecutionPolicy Bypass Process; irm https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.ps1 | Out-File -Encoding utf8 .\install.ps1; .\install.ps1; Remove-Item .\install.ps1 +``` + +
+ +#### Notes + +> 💡 **Tip**: Installation script automatically selects mirror sources based on geographic location. To explicitly specify, use `--cn` or `--official` parameters. See [Installation Script Parameters](#️-installation-script-parameters). -## Usage under Linux +> ⚠️ **Important**: +> - ✅ First installation requires administrator privileges for execution policy and long path support +> - 🦠 Antivirus software may block installation, please temporarily disable if needed -### Tutorial +#### Activate Environment -[How to install Env Tool with QEMU simulator in Ubuntu](https://github.com/RT-Thread/rt-thread/blob/master/documentation/quick-start/quick_start_qemu/quick_start_qemu_linux.md) +After installation, you need to activate environment variables to use the tools. -### Install Env +
+🔧 Option A: Manual Activation (Temporary) +Run the following command each time you start a new PowerShell session: + +```powershell +. ~/.rt-env/env.ps1 +``` + +
+ +
+⭐ Option B: Auto Activation (Recommended) + +Add the activation command to your PowerShell configuration file: + +```powershell +# Open configuration file (creates if it doesn't exist) +notepad $PROFILE + +# Add the following line: +. ~/.rt-env/env.ps1 +``` + +**Configuration File Paths:** + +| PowerShell Version | Configuration File Path | +|-------------------|------------------------| +| Windows PowerShell (v5.1) | `C:\Users\\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` | +| PowerShell 7+ | `C:\Users\\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` | + +After adding, the environment will be automatically activated each time you open PowerShell. + +
+ +### Linux/macOS Installation + +#### Installation Commands + +
+🌐 International Users (Official Source) + +```bash +bash -c "$(wget https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.sh -O -)" ``` -wget https://raw.githubusercontent.com/RT-Thread/env/master/install_ubuntu.sh -chmod 777 install_ubuntu.sh -./install_ubuntu.sh -rm install_ubuntu.sh + +
+ +
+🇨🇳 Users in China (Mirror Source) + +```bash +bash -c "$(wget https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.sh -O -)" -- --cn ``` -对于中国大陆用户,请使用以下命令 +
+ +#### Notes + +> 💡 **Tips**: +> - Installation script automatically selects mirror sources based on geographic location +> - Linux systems automatically use `sudo` to elevate privileges for installing dependencies, no need to manually run as root +> - Supports installation with normal user privileges, script automatically handles privilege elevation + +#### Activate Environment +
+🔧 Option A: Manual Activation (Temporary) + +Run the following command each time you open a new terminal: + +```bash +source ~/.rt-env/env.sh ``` -wget https://gitee.com/RT-Thread-Mirror/env/raw/master/install_ubuntu.sh -chmod 777 install_ubuntu.sh -./install_ubuntu.sh --gitee -rm install_ubuntu.sh + +
+ +
+⭐ Option B: Auto Activation (Recommended) + +Add the activation command to your shell configuration file: + +```bash +# For bash +echo 'source ~/.rt-env/env.sh' >> ~/.bashrc + +# For zsh +echo 'source ~/.rt-env/env.sh' >> ~/.zshrc ``` -### Prepare Env +After adding, the environment will be automatically activated each time you log in. -PLAN A: Whenever start the ubuntu system, you need to type command `source ~/.env/env.sh` to activate the environment variables. +
-or PLAN B: open `~/.bashrc` file, and attach the command `source ~/.env/env.sh` at the end of the file. It will be automatically executed when you log in the ubuntu, and you don't need to execute that command any more. +#### 📚 Related Tutorials -### Use Env +- [Install Env with QEMU Simulator in Ubuntu](https://github.com/RT-Thread/rt-thread/blob/master/documentation/quick-start/quick_start_qemu/quick_start_qemu_linux.md) -Please see: +--- -## Usage under Windows +## ⚙️ Installation Script Parameters -Tested on the following version of PowerShell: +| Parameter | Description | +|-----------|-------------| +| **Basic Parameters**|| +| `-y`, `--yes`, `--auto` | Automatic installation, no interaction | +| `-h`, `--help` | Display help information | +|**Source Settings**|| +| `-c`, `--cn`, `--gitee` | Use China mirror sources (Gitee, PyPI TUNA) | +| `-o`, `--official` | Force use of official sources | +| **Repository Configuration**|| +| `-P`, `--packages [#]` | Specify packages repository address and branch | +| `-E`, `--env [#]` | Specify env repository address and branch | +| `-S`, `--sdk [#]` | Specify sdk repository address and branch | +| `-t`, `--touch-env-url ` | Specify touch_env.py download URL | +|**Path & Installation**|| +| `-r`, `--env-root ` | Set custom .rt-env directory path (default: `~/.rt-env`) | +| `-p`, `--python [path]` | Install portable Python, installation directory is path (Windows only, default: D:\Tools\Python) | +|**Other Options**|| +| `-d`, `--pyocd` | Install pyocd (for debugging) | +| `-e`, `--en`, `--english` | Force English display | +| `-z`, `--zh`, `--chinese` | Force Chinese display | +| `-b`, `--backup ` | Backup strategy | -- PSVersion 5.1.22621.963 -- PSVersion 5.1.19041.2673 +### Backup Strategies -### Install Env +| Strategy | Description | +|----------|-------------| +| **preserve** (default) | Keep configuration files (.config) and toolchains (local_pkgs), delete other content | +| **delete_all** | Backup then delete existing ENV directory | +| **delete_all_now** | Immediately delete existing ENV directory, no backup | +| **backup_all** | Create full backup, keep all content | -您需要以管理员身份运行 PowerShell 来设置执行。(You need to run PowerShell as an administrator to set up execution.) +### Usage Examples -在 PowerShell 中执行(Execute the command in PowerShell): +
+💻 Windows (PowerShell) ```powershell -wget https://raw.githubusercontent.com/RT-Thread/env/master/install_windows.ps1 -O install_windows.ps1 -set-executionpolicy remotesigned -.\install_windows.ps1 +# Basic installation +.\install.ps1 + +# Use China mirror + automatic installation +.\install.ps1 -c -y + +# Install portable Python + custom path +.\install.ps1 -p "D:\Tools\Python" -r "D:\RT-Env" + +# Specify custom env repository branch +.\install.ps1 -E "https://github.com/RT-Thread/env.git#master" + +# Install pyocd + official source +.\install.ps1 -d -o +``` + +
+ +
+🐧 Linux/macOS (bash) + +```bash +# Basic installation +./install.sh + +# Use China mirror + automatic installation +./install.sh -c -y + +# Specify custom packages repository +./install.sh -P "https://gitee.com/RT-Thread/packages.git#master" + +# Use backup strategy +./install.sh -b preserve + +# Specify custom sdk repository +./install.sh -S "https://github.com/RT-Thread/sdk.git#master" ``` -对于中国大陆用户,请使用以下命令: +
+ +--- + +## 💡 Usage Guide + +After installation completes, follow these steps to start using RT-Thread ENV: + +### Step 1️⃣: Activate Environment + +
+💻 Windows ```powershell -wget https://gitee.com/RT-Thread-Mirror/env/raw/master/install_windows.ps1 -O install_windows.ps1 -set-executionpolicy remotesigned -.\install_windows.ps1 --gitee +. ~/.rt-env/env.ps1 ``` -注意: +
+ +
+🐧 Linux/macOS + +```bash +source ~/.rt-env/env.sh +``` + +
+ +> 💡 **Tip**: For auto-activation on startup, please refer to the auto-activation options in the installation section. + +### Step 2️⃣: Install Toolchains + +Run the `sdk` command to install required toolchains for your development board: + +```bash +sdk +``` + +### Step 3️⃣: Use Commands + +After activation, you can use the following commands: + +| Command | Function | Description | +|---------|----------|-------------| +| `menuconfig` | ⚙️ Configure Project | Configure RT-Thread kernel, BSP | +| `menuconfig -s` | 🔧 Configure ENV | Configure packages, tools | +| `pkgs` | 📦 Package Manager | Update, upgrade packages | +| `sdk` | 🛠️ Toolchain Manager | Install development toolchains | +| `scons` | 🔨 Build Project | Build project | + +### Step 4️⃣: Additional Tools (Optional) + +**pyocd** - For debugging Cortex-M devices: + +```bash +pip install pyocd +``` + +### 📚 Detailed Documentation + +- [Env Tool Usage Guide](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- [Env Official User Manual](https://www.rt-thread.org/document/site/#/development-tools/env/env) + +--- + +## ❓ Troubleshooting + +### 🌐 Network & Mirror Issues + +**Problem: Slow download or failure** + +- ✅ Use `--cn` parameter to enable Gitee mirror +- ✅ Check network connection +- ✅ Try switching network environment (e.g., using VPN) + +### 🔐 Permission Issues + +#### Linux/macOS + +Linux installation script automatically uses `sudo` for privilege elevation, usually no manual handling needed. + +If you encounter permission issues: + +```bash +# Check .rt-env directory permissions +ls -la ~/.rt-env + +# If directory belongs to root, change ownership +sudo chown -R $USER:$USER ~/.rt-env +``` + +#### Windows + +If you encounter permission errors: + +1. ✅ Check if antivirus software is blocking installation +2. ✅ Run PowerShell as administrator +3. ✅ Ensure execution policy allows script running + +### 📝 Other Issues + +If you encounter other issues, please: + +- 📖 Check [Env Tool Complete Documentation](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- 🐛 Submit issue on [GitHub Issues](https://github.com/RT-Thread/env/issues) +- 💬 Join [RT-Thread Forum](https://www.rt-thread.org/qa/forum.html) for help + +--- + +## 📖 Resources + +### Official Documentation + +- [Env Tool Complete Documentation](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- [QEMU Quick Start](https://github.com/RT-Thread/rt-thread/blob/master/documentation/quick-start/quick_start_qemu/quick_start_qemu_linux.md) +- [BSP Configuration Guide](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md#bsp-configuration-menuconfig) + +### Related Links + +| Type | Link | Mirror | +|------|------|--------| +| 📦 Env Repository | [GitHub](https://github.com/RT-Thread/env) | [Gitee](https://gitee.com/RT-Thread-Mirror/env) | +| ![RT-Thread](https://www.rt-thread.org/favicon.ico) RT-Thread Repository | [GitHub](https://github.com/RT-Thread/rt-thread) | [Gitee](https://gitee.com/rtthread/rt-thread) | +| 🌐 Official Website | [RT-Thread Website](https://www.rt-thread.org/) || +| 📚 Documentation | [RT-Thread Docs (EN)](https://www.rt-thread.io/document/site/) | [RT-Thread Docs (CN)](https://www.rt-thread.org/document/site/#/) | + +### License + +[![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) + +This project is open-sourced under the **GPL-2.0** license. + +--- + +
-1. Powershell要以管理员身份运行。 -2. 将其设置为 remotesigned 后,您可以作为普通用户运行 PowerShell。( After setting it to remotesigned, you can run PowerShell as a normal user.) -3. 一定要关闭杀毒软件,否则安装过程可能会被杀毒软件强退 +## 🤝 Contributors -### Prepare Env +Thanks to all developers who have contributed to the RT-Thread Env project! -方案 A:每次重启 PowerShell 时,都需要输入命令 `~/.env/env.ps1`,以激活环境变量。(PLAN A: Each time you restart PowerShell, you need to enter the command `~/.env/env.ps1` to activate the environment variable.) +[![Contributors](https://img.shields.io/github/contributors/RT-Thread/env?style=for-the-badge)](https://github.com/RT-Thread/env/graphs/contributors) -方案 B (推荐):打开 `C:\Users\user\Documents\WindowsPowerShell`,如果没有`WindowsPowerShell`则新建该文件夹。新建文件 `Microsoft.PowerShell_profile.ps1`,然后写入 `~/.env/env.ps1` 内容即可,它将在你重启 PowerShell 时自动执行,无需再执行方案 A 中的命令。(or PLAN B (recommended): Open `C:\Users\user\Documents\WindowsPowerShell` and create a new file `Microsoft.PowerShell_profile.ps1`. Then write `~/.env/env.ps1` to the file. It will be executed automatically when you restart PowerShell, without having to execute the command in scenario A.) +
\ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 00000000..6d96d21f --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,417 @@ +
+ +# RT-Thread Env Python 脚本 + +[![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) +[![Python](https://img.shields.io/badge/Python-3.6+-green.svg)](https://www.python.org/) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]() + +**[English](README.md) | 简体中文** + +
+ +--- + +## ⚠️ 版本兼容性提示 + +| RT-Thread 版本 | 推荐使用 Env 版本 | +|----------------|-------------------| +| **> v5.1.0** 或 **master 分支** | ✅ **env v2.0** (当前版本) | +| **≤ v5.1.0** | ⚠️ [env v1.5.x](https://github.com/RT-Thread/env/tree/v1.5.x) (Linux) / [env-windows v1.5.2](https://github.com/RT-Thread/env-windows/tree/v1.5.2) (Windows) | + +### 🔄 v2.0 主要变更 + +- ✨ **Python 版本升级**:从 Python 2 升级到 Python 3 +- 🔧 **配置系统重构**:使用 Python kconfiglib 替代 kconfig-frontends +- 📦 **依赖管理优化**:安装程序自动处理 kconfiglib 依赖 + +> **注意**:env v2.0 与 env v1.5.x 的 kconfiglib 不兼容。如需切换版本,请先运行 `pip uninstall kconfiglib`。 + +--- + +## 📚 目录 + +- [🚀 快速开始](#-快速开始) + - [Windows 安装](#windows-安装) + - [Linux/macOS 安装](#linuxmacos-安装) +- [⚙️ 安装脚本参数](#️-安装脚本参数) +- [💡 使用指南](#-使用指南) +- [❓ 常见问题](#-常见问题) +- [📖 相关资源](#-相关资源) + +--- + +## 🚀 快速开始 + +### Windows 安装 + +#### 前置要求 + +| 项目 | 要求 | +|------|------| +| **权限** | 📋 首次安装需**管理员权限**(设置执行策略和长路径支持)
后续更新可使用普通用户权限 | +| **PowerShell** | ✅ Windows PowerShell v5.1+
✅ PowerShell 7+ | + +#### 编码兼容性说明 + +> ⚠️ **重要提示** +> +> | PowerShell 版本 | 编码支持 | 说明 | +> |----------------|----------|------| +> | Windows PowerShell (v5.1) | ⚠️ GB2312 默认,需 UTF-8 with BOM | 中文系统需特殊处理 | +> | PowerShell 7+ | ✅ 原生 UTF-8 | 无编码问题 | +> +> 安装脚本已配置为 UTF-8 with BOM 编码,确保兼容性。 + +#### 安装命令 + +
+🌐 国际用户(官方源) + +```powershell +Set-ExecutionPolicy -ExecutionPolicy Bypass Process; irm https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.ps1 | Out-File -Encoding utf8 .\install.ps1; .\install.ps1; Remove-Item .\install.ps1 +``` + +
+ +
+🇨🇳 中国大陆用户(镜像源) + +```powershell +Set-ExecutionPolicy -ExecutionPolicy Bypass Process; irm https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.ps1 | Out-File -Encoding utf8 .\install.ps1; .\install.ps1; Remove-Item .\install.ps1 +``` + +
+ +#### 注意事项 + +> 💡 **提示**:安装脚本会根据地理位置自动选择镜像源。如需明确指定,请使用 `--cn` 或 `--official` 参数。详见 [安装脚本参数](#️-安装脚本参数)。 + +> ⚠️ **重要**: +> - ✅ 首次安装需管理员权限设置执行策略和长路径支持 +> - 🦠 杀毒软件可能会阻止安装,如有需要请暂时禁用 + +#### 激活环境 + +安装完成后,需要激活环境变量才能使用。 + +
+🔧 方案 A:手动激活(临时) + +每次启动新的 PowerShell 会话时运行: + +```powershell +. ~/.rt-env/env.ps1 +``` + +
+ +
+⭐ 方案 B:自动激活(推荐) + +将激活命令添加到 PowerShell 配置文件: + +```powershell +# 打开配置文件(如不存在则创建) +notepad $PROFILE + +# 添加以下行: +. ~/.rt-env/env.ps1 +``` + +**配置文件路径:** + +| PowerShell 版本 | 配置文件路径 | +|----------------|-------------| +| Windows PowerShell (v5.1) | `C:\Users\<用户名>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` | +| PowerShell 7+ | `C:\Users\<用户名>\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` | + +添加后,每次打开 PowerShell 会自动激活环境。 + +
+ +### Linux/macOS 安装 + +#### 安装命令 + +
+🌐 国际用户(官方源) + +```bash +bash -c "$(wget https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.sh -O -)" +``` + +
+ +
+🇨🇳 中国大陆用户(镜像源) + +```bash +bash -c "$(wget https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.sh -O -)" -- --cn +``` + +
+ +#### 注意事项 + +> 💡 **提示**: +> - 安装脚本会根据地理位置自动选择镜像源 +> - Linux 系统会自动使用 `sudo` 提权安装依赖,无需手动以 root 运行 +> - 支持普通用户权限安装,脚本会自动处理权限提升 + +#### 激活环境 + +
+🔧 方案 A:手动激活(临时) + +每次打开新终端时运行: + +```bash +source ~/.rt-env/env.sh +``` + +
+ +
+⭐ 方案 B:自动激活(推荐) + +将激活命令添加到 shell 配置文件: + +```bash +# 对于 bash +echo 'source ~/.rt-env/env.sh' >> ~/.bashrc + +# 对于 zsh +echo 'source ~/.rt-env/env.sh' >> ~/.zshrc +``` + +添加后,每次登录系统时自动激活环境。 + +
+ +#### 📚 相关教程 + +- [在 Ubuntu 中安装 Env 并配合 QEMU 模拟器使用](https://github.com/RT-Thread/rt-thread/blob/master/documentation/quick-start/quick_start_qemu/quick_start_qemu_linux.md) + +--- + +## ⚙️ 安装脚本参数 + +| 参数 | 描述 | +|------|------| +| **基础参数**|| +| `-y`, `--yes`, `--auto` | 自动安装,无交互 | +| `-h`, `--help` | 显示帮助信息 | +|**源设置**|| +| `-c`, `--cn`, `--gitee` | 使用中国镜像源(Gitee、PyPI TUNA) | +| `-o`, `--official` | 强制使用官方源 | +| **仓库配置**|| +| `-P`, `--packages [#]` | 指定 packages 仓库地址和分支 | +| `-E`, `--env [#]` | 指定 env 仓库地址和分支 | +| `-S`, `--sdk [#]` | 指定 sdk 仓库地址和分支 | +| `-t`, `--touch-env-url ` | 指定 touch_env.py 下载 URL | +|**路径与安装**|| +| `-r`, `--env-root ` | 设置自定义 .rt-env 目录路径(默认:`~/.rt-env`) | +| `-p`, `--python [path]` | 安装便携式 Python,安装目录为 path(仅 Windows,默认:D:\Tools\Python) | +|**其他选项**|| +| `-d`, `--pyocd` | 安装 pyocd(用于调试) | +| `-e`, `--en`, `--english` | 强制英文显示 | +| `-z`, `--zh`, `--chinese` | 强制中文显示 | +| `-b`, `--backup ` | 备份策略 | + +### 备份策略 + +| 策略 | 说明 | +|------|------| +| **preserve** (默认) | 保留配置文件(.config)和工具链(local_pkgs),删除其他内容 | +| **delete_all** | 备份后删除现有 ENV 目录 | +| **delete_all_now** | 立即删除现有 ENV 目录,不备份 | +| **backup_all** | 创建完整备份,保留所有内容 | + +### 使用示例 + +
+💻 Windows (PowerShell) + +```powershell +# 基本安装 +.\install.ps1 + +# 使用中国镜像 + 自动安装 +.\install.ps1 -c -y + +# 安装便携式 Python + 自定义路径 +.\install.ps1 -p "D:\Tools\Python" -r "D:\RT-Env" + +# 指定自定义 env 仓库分支 +.\install.ps1 -E "https://github.com/RT-Thread/env.git#master" + +# 安装 pyocd + 使用官方源 +.\install.ps1 -d -o +``` + +
+ +
+🐧 Linux/macOS (bash) + +```bash +# 基本安装 +./install.sh + +# 使用中国镜像 + 自动安装 +./install.sh -c -y + +# 指定自定义 packages 仓库 +./install.sh -P "https://gitee.com/RT-Thread/packages.git#master" + +# 使用备份策略 +./install.sh -b preserve + +# 指定自定义 sdk 仓库 +./install.sh -S "https://github.com/RT-Thread/sdk.git#master" +``` + +
+ +--- + +## 💡 使用指南 + +安装完成后,按照以下步骤开始使用 RT-Thread ENV: + +### 步骤 1️⃣:激活环境 + +
+💻 Windows + +```powershell +. ~/.rt-env/env.ps1 +``` + +
+ +
+🐧 Linux/macOS + +```bash +source ~/.rt-env/env.sh +``` + +
+ +> 💡 **提示**:如需每次启动自动激活,请参考安装章节的自动激活方案。 + +### 步骤 2️⃣:安装工具链 + +运行 `sdk` 命令安装开发板所需的工具链: + +```bash +sdk +``` + +### 步骤 3️⃣:使用命令 + +激活后,可以使用以下命令: + +| 命令 | 功能 | 说明 | +|------|------|------| +| `menuconfig` | ⚙️ 配置项目 | 配置 RT-Thread 内核、BSP | +| `menuconfig -s` | 🔧 配置 ENV | 配置软件包、工具 | +| `pkgs` | 📦 包管理器 | 更新、升级软件包 | +| `sdk` | 🛠️ 工具链管理器 | 安装开发工具链 | +| `scons` | 🔨 编译项目 | 构建项目 | + +### 步骤 4️⃣:额外工具(可选) + +**pyocd** - 用于调试 Cortex-M 设备: + +```bash +pip install pyocd +``` + +### 📚 详细文档 + +- [Env 工具使用指南](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- [Env 官方用户手册](https://www.rt-thread.org/document/site/#/development-tools/env/env) + +--- + +## ❓ 常见问题 + +### 🌐 网络与镜像问题 + +**问题:下载缓慢或失败** + +- ✅ 使用 `--cn` 参数启用 Gitee 镜像 +- ✅ 检查网络连接 +- ✅ 尝试切换网络环境(如使用 VPN) + +### 🔐 权限问题 + +#### Linux/macOS + +Linux 安装脚本会自动使用 `sudo` 提权,通常无需手动处理。 + +如遇权限问题: + +```bash +# 检查 .rt-env 目录权限 +ls -la ~/.rt-env + +# 如果目录属于 root,修改所有权 +sudo chown -R $USER:$USER ~/.rt-env +``` + +#### Windows + +如遇权限错误: + +1. ✅ 检查杀毒软件是否阻止安装 +2. ✅ 以管理员身份运行 PowerShell +3. ✅ 确保执行策略允许脚本运行 + +### 📝 其他问题 + +如遇到其他问题,请: + +- 📖 查看 [Env 工具完整文档](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- 🐛 在 [GitHub Issues](https://github.com/RT-Thread/env/issues) 提交问题 +- 💬 加入 [RT-Thread 论坛](https://www.rt-thread.org/qa/forum.html) 寻求帮助 + +--- + +## 📖 相关资源 + +### 官方文档 + +- [Env 工具完整文档](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md) +- [QEMU 快速入门](https://github.com/RT-Thread/rt-thread/blob/master/documentation/quick-start/quick_start_qemu/quick_start_qemu_linux.md) +- [BSP 配置说明](https://github.com/RT-Thread/rt-thread/blob/master/documentation/env/env.md#bsp-configuration-menuconfig) + +### 相关链接 + +| 类型 | 链接 |镜像| +|------|------|----| +| 📦 Env 仓库 | [GitHub](https://github.com/RT-Thread/env) |[Gitee](https://gitee.com/RT-Thread-Mirror/env) | +| ![RT-Thread](https://www.rt-thread.org/favicon.ico) RT-Thread 仓库|[GitHub](https://github.com/RT-Thread/rt-thread) |[Gitee](https://gitee.com/rtthread/rt-thread) | +| 🌐 官方网站 | [RT-Thread 官网](https://www.rt-thread.org/) || +| 📚 文档中心 | [RT-Thread 文档(英文)](https://www.rt-thread.io/document/site/) |[RT-Thread 文档中心(中文)](https://www.rt-thread.org/document/site/#/)| + +### 许可证 + +[![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) + +本项目采用 **GPL-2.0** 许可证开源。 + +--- + +
+ +## 🤝 贡献者 + +感谢所有为 RT-Thread Env 项目做出贡献的开发者! + +[![Contributors](https://img.shields.io/github/contributors/RT-Thread/env?style=for-the-badge)](https://github.com/RT-Thread/env/graphs/contributors) + +
\ No newline at end of file diff --git a/env.ps1 b/env.ps1 index dd4cc7f4..50fc12a3 100644 --- a/env.ps1 +++ b/env.ps1 @@ -1,30 +1,23 @@ -$VENV_ROOT = "$PSScriptRoot\.venv" -# rt-env目录是否存在 -if (-not (Test-Path -Path $VENV_ROOT)) { - Write-Host "Create Python venv for RT-Thread..." - python -m venv $VENV_ROOT - # 激活python venv - & "$VENV_ROOT\Scripts\Activate.ps1" - # 安装env-script - # 判断IP是否在中国大陆,若是则用清华源,否则用默认源 - try { - $china = $false - $ipinfo = Invoke-RestMethod -Uri "https://ipinfo.io/json" -UseBasicParsing -TimeoutSec 3 - if ($ipinfo.country -eq "CN") { - $china = $true - } - } catch { - $china = $false - } - if ($china) { - Write-Host "Detected China Mainland IP, using Tsinghua PyPI mirror." - pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "$PSScriptRoot\tools\scripts" - } else { - pip install "$PSScriptRoot\tools\scripts" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +# Overridden by $env:ENV_ROOT environment variable +$env:ENV_ROOT = $PSScriptRoot + +# Virtual environment directory name +$RT_VENV_DIR = "$env:ENV_ROOT\venv\rt-env" + +if (Test-Path "$RT_VENV_DIR\Scripts\Activate.ps1") { + . "$RT_VENV_DIR\Scripts\Activate.ps1" + + # Show welcome message using rt-env command + if (Get-Command rt-env -ErrorAction SilentlyContinue) { + rt-env -v } -} else { - # 激活python venv - & "$VENV_ROOT\Scripts\Activate.ps1" +} +else { + Write-Host "Virtual environment($RT_VENV_DIR\Scripts\Activate.ps1) not found. Please run the installation `RT-Thread ENV` first." + exit 1 } $env:pathext = ".PS1;$env:pathext" diff --git a/env.py b/env.py index d2404765..be81332a 100644 --- a/env.py +++ b/env.py @@ -40,15 +40,29 @@ from vars import Export from version import get_rt_env_version -def show_version_warning(): +def show_version(): rtt_ver = get_rtt_verion() rt_env_name, rt_env_ver = get_rt_env_version() + + print('\033[1;36m===================================================================\033[0m') + print('\033[1;36m Welcome to %s %s\033[0m' % (rt_env_name, rt_env_ver)) + print('\033[1;36m===================================================================\033[0m') + print('Environment Information:') + print(' - ENV_ROOT: %s' % get_env_root()) + print(' - PKGS_ROOT: %s' % get_package_root()) + + if rtt_ver != (0, 0, 0): + print(' - RTT_ROOT: %s' % get_rtt_root()) + print(' - BSP_ROOT: %s' % get_bsp_root()) + print(' - RT-Thread Version: %d.%d.%d' % rtt_ver) + print('\033[1;36m===================================================================\033[0m') + +def show_version_warning(is_show_version=True): + rtt_ver = get_rtt_verion() if rtt_ver <= (5, 1, 0) and rtt_ver != (0, 0, 0): - print('===================================================================') - print('Welcome to %s %s' % (rt_env_name, rt_env_ver)) - print('===================================================================') - # print('') + if is_show_version: + show_version() print('env v2.0 has made the following important changes:') print('1. Upgrading Python version from v2 to v3') print('2. Replacing kconfig-frontends with Python kconfiglib') @@ -72,7 +86,9 @@ def init_argparse(): rt_env_name, rt_env_ver = get_rt_env_version() env_ver_str = '%s %s' % (rt_env_name, rt_env_ver) - parser.add_argument('-v', '--version', action='version', version=env_ver_str) + + # Override -v to show welcome message instead of version + parser.add_argument('-v', '--version', action='store_true', help='Show environment information') cmd_system.add_parser(subs) cmd_menuconfig.add_parser(subs) @@ -216,18 +232,31 @@ def exec_arg(arg): args.func(args) -def main(): - show_version_warning() - export_environment_variable() - init_logger(get_env_root()) +def cmd_version(args): + """Handle version display.""" + show_version() + show_version_warning(False) + sys.exit(0) + +def main(): parser = init_argparse() args = parser.parse_args() - if not vars(args): + if args.version: + cmd_version(args) + + # Check if any subcommand was provided + if not hasattr(args, 'func'): + # No subcommand provided, show help parser.print_help() - else: - args.func(args) + exit(0) + + show_version_warning() + export_environment_variable() + init_logger(get_env_root()) + + args.func(args) def menuconfig(): diff --git a/env.sh b/env.sh index 5c5280f8..2dc93380 100644 --- a/env.sh +++ b/env.sh @@ -1 +1,24 @@ -export PATH=~/.env/tools/scripts:$PATH +# Can be overridden by $ENV_ROOT environment variable +# Get script directory as default ENV_ROOT +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export "ENV_ROOT=$SCRIPT_DIR" + +# Virtual environment directory name +RT_VENV_DIR="$ENV_ROOT/venv/rt-env" + +# Activate Python virtual environment +if [ -f "$RT_VENV_DIR/bin/activate" ]; then + source "$RT_VENV_DIR/bin/activate" + + # Show welcome message using rt-env command + if command -v rt-env &> /dev/null; then + rt-env -v + fi +else + echo "Virtual environment not found. Please run the installation script first." + return 1 +fi + +# Set PATH +# export PATH="$ENV_ROOT/tools/scripts:$PATH" +export RTT_EXEC_PATH=/usr/bin diff --git a/install_arch.sh b/install_arch.sh deleted file mode 100755 index cc6fe9f3..00000000 --- a/install_arch.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -# 函数:从 AUR 安装 rt-thread-env-meta 包 -install_from_aur() { - echo "正在从 AUR 安装 rt-thread-env-meta 包..." - yay -Syu rt-thread-env-meta -} - -# 函数:手动安装所需的包 -install_manually() { - echo "正在手动安装所需的包..." - - # 安装基本依赖包 - sudo pacman -Syu python python-pip gcc git ncurses \ - arm-none-eabi-gcc arm-none-eabi-gdb \ - qemu-desktop qemu-system-arm-firmware scons \ - python-requests python-tqdm python-kconfiglib - - # 提示用户安装 python-pyocd 及其插件 - echo " - # python-pyocd 可以通过 AUR 安装或从 GitHub 获取: - # https://github.com/taotieren/aur-repo - yay -Syu python-pyocd python-pyocd-pemicro - " - - # 询问用户是否要继续安装 python-pyocd - read -p "是否现在安装 python-pyocd 和 python-pyocd-pemicro? (y/n) " choice - case "$choice" in - y | Y) - yay -Syu python-pyocd python-pyocd-pemicro - ;; - n | N) - echo "跳过安装 python-pyocd 和 python-pyocd-pemicro." - ;; - *) - echo "无效输入,跳过安装 python-pyocd 和 python-pyocd-pemicro." - ;; - esac -} - -# 显示菜单供用户选择 -echo "请选择安装方式:" -echo "1. 从 AUR 安装 rt-thread-env-meta 包" -echo "2. 手动安装所有所需包" -read -p "请输入选项 [1 或 2]: " option - -case $option in -1) - install_from_aur - ;; -2) - install_manually - ;; -*) - echo "无效选项,退出安装程序。" - exit 1 - ;; -esac - -echo "安装完成。" - -url=https://raw.githubusercontent.com/RT-Thread/env/master/touch_env.sh -if [ $1 ] && [ $1 = --gitee ]; then - url=https://gitee.com/RT-Thread-Mirror/env/raw/master/touch_env.sh -fi - -wget $url -O touch_env.sh -chmod 777 touch_env.sh -./touch_env.sh $@ -rm touch_env.sh diff --git a/install_macos.sh b/install_macos.sh deleted file mode 100755 index 60984795..00000000 --- a/install_macos.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash - -RTT_PYTHON=python - -for p_cmd in python3 python; do - $p_cmd --version >/dev/null 2>&1 || continue - RTT_PYTHON=$p_cmd - break -done - -$RTT_PYTHON --version 2 >/dev/null || { - echo "Python not installed. Please install Python before running the installation script." - exit 1 -} - -if ! [ -x "$(command -v brew)" ]; then - echo "Installing Homebrew." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -fi - -brew update -brew upgrade - -if ! [ -x "$(command -v git)" ]; then - echo "Installing git." - brew install git -fi - -brew list ncurses >/dev/null || { - echo "Installing ncurses." - brew install ncurses -} - -$RTT_PYTHON -m pip list >/dev/null || { - echo "Installing pip." - $RTT_PYTHON -m ensurepip --upgrade -} - -if ! [ -x "$(command -v scons)" ]; then - echo "Installing scons." - $RTT_PYTHON -m pip install scons -fi - -if ! [ -x "$(command -v tqdm)" ]; then - echo "Installing tqdm." - $RTT_PYTHON -m pip install tqdm -fi - -if ! [ -x "$(command -v kconfiglib)" ]; then - echo "Installing kconfiglib." - $RTT_PYTHON -m pip install kconfiglib -fi - -if ! [ -x "$(command -v pyocd)" ]; then - echo "Installing pyocd." - $RTT_PYTHON -m pip install -U pyocd -fi - -if ! [[ $($RTT_PYTHON -m pip list | grep requests) ]]; then - echo "Installing requests." - $RTT_PYTHON -m pip install requests -fi - -if ! [ -x "$(command -v arm-none-eabi-gcc)" ]; then - echo "Installing GNU Arm Embedded Toolchain." - brew install gnu-arm-embedded -fi - -url=https://raw.githubusercontent.com/RT-Thread/env/master/touch_env.sh -if [ $1 ] && [ $1 = --gitee ]; then - url=https://gitee.com/RT-Thread-Mirror/env/raw/master/touch_env.sh -fi -curl $url -o touch_env.sh -chmod 777 touch_env.sh -./touch_env.sh $@ -rm touch_env.sh diff --git a/install_suse.sh b/install_suse.sh deleted file mode 100755 index a127718b..00000000 --- a/install_suse.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -sudo zypper update -y - -sudo zypper install python3 python3-pip gcc git ncurses-devel cross-arm-none-gcc11-bootstrap cross-arm-binutils qemu qemu-arm qemu-extra -y -python3 -m pip install scons requests tqdm kconfiglib -python3 -m pip install -U pyocd - -url=https://raw.githubusercontent.com/RT-Thread/env/master/touch_env.sh -if [ $1 ] && [ $1 = --gitee ]; then - url=https://gitee.com/RT-Thread-Mirror/env/raw/master/touch_env.sh -fi - -wget $url -O touch_env.sh -chmod 777 touch_env.sh -./touch_env.sh $@ -rm touch_env.sh diff --git a/install_ubuntu.sh b/install_ubuntu.sh index 2d27e971..772ca1de 100755 --- a/install_ubuntu.sh +++ b/install_ubuntu.sh @@ -1,15 +1,138 @@ #!/usr/bin/env bash +# +# DEPRECATED / 已废弃 +# +# 此脚本已废弃,推荐直接使用 tools/install.sh +# +# Ubuntu Quick Install Script (Deprecated) +# Usage: +# ./install_ubuntu.sh # Auto-install (auto-detect mirror) +# ./install_ubuntu.sh --cn # Auto-install (China mirror) +# ./install_ubuntu.sh --gitee # Auto-install (Gitee) +# +# Deprecated: Please use tools/install.sh directly +# curl https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.sh | bash -s -- -y +# curl https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.sh | bash -s -- -y --cn +# +# This script maintains backward compatibility with old versions while +# delegating to the new unified install.sh script +# -sudo apt-get update -sudo apt-get -qq install python3 python3-pip gcc git libncurses5-dev -y -pip install scons requests tqdm kconfiglib pyyaml +set -e -url=https://raw.githubusercontent.com/RT-Thread/env/master/touch_env.sh -if [ $1 ] && [ $1 = --gitee ]; then - url=https://gitee.com/RT-Thread-Mirror/env/raw/master/touch_env.sh +# ============================================================================ +# Configuration +# ============================================================================ + +# URL configurations +URL_GITHUB="https://raw.githubusercontent.com/RT-Thread/env/master/tools/install.sh" +URL_GITEE="https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/install.sh" + +# IP detection service +IPINFO_URL="https://ipinfo.io/json" + +# Environment directory (compatible with old versions - use .env by default) +ENV_DEFAULT_DIR=".env" +: "${ENV_ROOT:=$HOME/$ENV_DEFAULT_DIR}" + +# ============================================================================ +# Activate Virtual Environment (for backward compatibility) +# ============================================================================ + +activate_venv() { + # Activate the virtual environment if it exists + local venv_path="$ENV_ROOT/venv/rt-env/bin/activate" + if [ -f "$venv_path" ]; then + source "$venv_path" + echo "✓ Virtual environment activated" + else + echo "⚠ Virtual environment not found at $venv_path" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +# Show deprecation notice +echo "============================================================" +echo " DEPRECATED / 已废弃" +echo "============================================================" +echo "" +echo "此脚本已废弃,推荐直接使用 tools/install.sh" +echo "This script is deprecated, please use tools/install.sh directly" +echo "" +echo "使用 GitHub / Using GitHub:" +echo " curl $URL_GITHUB | bash -s -- -y" +echo "" +echo "使用中国镜像 / Using China Mirror:" +echo " curl $URL_GITEE | bash -s -- -y --cn" +echo "" +echo "============================================================" +echo "" + +# Parse arguments +USE_CN="" +USE_CN_SET="false" +OTHER_ARGS="" + +for arg in "$@"; do + case "$arg" in + --cn|--gitee) + USE_CN="true" + USE_CN_SET="true" + ;; + --no-mirror) + USE_CN="false" + USE_CN_SET="true" + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --cn, --gitee Use China mirror (Gitee)" + echo " --no-mirror Force use official GitHub source" + echo " --help, -h Show this help" + echo "" + echo "This script downloads and executes the new install.sh with" + echo "backward compatibility settings for old .env path and GitHub Actions." + exit 0 + ;; + *) + OTHER_ARGS="$OTHER_ARGS $arg" + ;; + esac +done + +# Auto-detect China if not explicitly set +if [[ "$USE_CN_SET" == "false" ]]; then + USE_CN=$(detect_china) +fi + +# Determine URL +if [[ "$USE_CN" == "true" ]]; then + INSTALL_URL="$URL_GITEE" +else + INSTALL_URL="$URL_GITHUB" fi -wget $url -O touch_env.sh -chmod 777 touch_env.sh -./touch_env.sh $@ -rm touch_env.sh +echo "检测到位置: $([ "$USE_CN" == "true" ] && echo "中国大陆" || echo "其他地区")" +echo "下载地址: $INSTALL_URL" +echo "" + +# Download and execute install.sh directly (without writing to disk) +wget -qO- "$INSTALL_URL" | bash -s -- -y --env-root "$ENV_ROOT" $OTHER_ARGS + +# Activate virtual environment after installation +if [ -d "$ENV_ROOT" ]; then + echo "" + echo "============================================================" + echo "激活虚拟环境 / Activating Virtual Environment" + echo "============================================================" + echo "" + activate_venv + echo "" + echo "To activate the environment manually, run:" + echo " source $ENV_ROOT/env.sh" + echo "" +fi diff --git a/install_windows.ps1 b/install_windows.ps1 deleted file mode 100644 index 8ed3b779..00000000 --- a/install_windows.ps1 +++ /dev/null @@ -1,131 +0,0 @@ - -$RTT_PYTHON = "python" - -function Test-Command( [string] $CommandName ) { - (Get-Command $CommandName -ErrorAction SilentlyContinue) -ne $null -} - -foreach ($p_cmd in ("python3", "python", "py")) { - cmd /c $p_cmd --version | findstr "Python" | Out-Null - if (!$?) { continue } - $RTT_PYTHON = $p_cmd - break -} - -cmd /c $RTT_PYTHON --version | findstr "Python" | Out-Null -if (!$?) { - echo "Python is not installed. Will install python 3.11.2." - echo "Downloading Python." - wget -O Python_setup.exe https://www.python.org/ftp/python/3.11.2/python-3.11.2.exe - echo "Installing Python." - if (Test-Path -Path "D:\") { - cmd /c Python_setup.exe /quiet TargetDir=D:\Progrem\Python311 InstallAllUsers=1 PrependPath=1 Include_test=0 - } else { - cmd /c Python_setup.exe /quiet PrependPath=1 Include_test=0 - } - echo "Install Python done. please close the current terminal and run this script again." - exit -} else { - echo "Python environment has installed. Jump this step." -} - -$git_url = "https://github.com/git-for-windows/git/releases/download/v2.39.2.windows.1/Git-2.39.2-64-bit.exe" -if ($args[0] -eq "--gitee") { - echo "Use gitee mirror server!" - $git_url = "https://registry.npmmirror.com/-/binary/git-for-windows/v2.39.2.windows.1/Git-2.39.2-64-bit.exe" -} - -if (!(Test-Command git)) { - echo "Git is not installed. Will install Git." - echo "Installing git." - winget install --id Git.Git -e --source winget - if (!$?) { - echo "Can't find winget cmd, Will install git 2.39.2." - echo "downloading git." - wget -O Git64.exe $git_url - echo "Please install git. when install done, close the current terminal and run this script again." - cmd /c Git64.exe /quiet PrependPath=1 - exit - } -} else { - echo "Git environment has installed. Jump this step." -} - -$PIP_SOURCE = "https://pypi.org/simple" -$PIP_HOST = "pypi.org" -if ($args[0] -eq "--gitee") { - $PIP_SOURCE = "http://mirrors.aliyun.com/pypi/simple" - $PIP_HOST = "mirrors.aliyun.com" -} - -cmd /c $RTT_PYTHON -m pip list -i $PIP_SOURCE --trusted-host $PIP_HOST | Out-Null -if (!$?) { - echo "Installing pip." - cmd /c $RTT_PYTHON -m ensurepip --upgrade -} else { - echo "Pip has installed. Jump this step." -} - -cmd /c $RTT_PYTHON -m pip install --upgrade pip -i $PIP_SOURCE --trusted-host $PIP_HOST | Out-Null - -if (!(Test-Command scons)) { - echo "Installing scons." - cmd /c $RTT_PYTHON -m pip install scons -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "scons has installed. Jump this step." -} - -if (!(Test-Command pyocd)) { - echo "Installing pyocd." - cmd /c $RTT_PYTHON -m pip install -U pyocd -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "pyocd has installed. Jump this step." -} - -cmd /c $RTT_PYTHON -m pip list -i $PIP_SOURCE --trusted-host $PIP_HOST | findstr "tqdm" | Out-Null -if (!$?) { - echo "Installing tqdm module." - cmd /c $RTT_PYTHON -m pip install tqdm -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "tqdm module has installed. Jump this step." -} - -cmd /c $RTT_PYTHON -m pip list -i $PIP_SOURCE --trusted-host $PIP_HOST | findstr "kconfiglib" | Out-Null -if (!$?) { - echo "Installing kconfiglib module." - cmd /c $RTT_PYTHON -m pip install kconfiglib -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "kconfiglib module has installed. Jump this step." -} - - -cmd /c $RTT_PYTHON -m pip list -i $PIP_SOURCE --trusted-host $PIP_HOST | findstr "requests" | Out-Null -if (!$?) { - echo "Installing requests module." - cmd /c $RTT_PYTHON -m pip install requests -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "requests module has installed. Jump this step." -} - -cmd /c $RTT_PYTHON -m pip list -i $PIP_SOURCE --trusted-host $PIP_HOST | findstr "psutil" | Out-Null -if (!$?) { - echo "Installing psutil module." - cmd /c $RTT_PYTHON -m pip install psutil -i $PIP_SOURCE --trusted-host $PIP_HOST -} else { - echo "psutil module has installed. Jump this step." -} - -$url = "https://raw.githubusercontent.com/RT-Thread/env/master/touch_env.ps1" -if ($args[0] -eq "--gitee") { - $url = "https://gitee.com/RT-Thread-Mirror/env/raw/master/touch_env.ps1" -} - -wget $url -O touch_env.ps1 -echo "run touch_env.ps1" -./touch_env.ps1 $args[0] - -if ($args.Count -ge 2 -and $args[1] -eq "-y") { - echo "Windows Env environment installment has finished. (auto mode, no pause)" -} else { - Read-Host -Prompt "Windows Env environment installment has finished. Press any key to continue..." -} diff --git a/pyproject.toml b/pyproject.toml index 3deafd54..b07b208f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,33 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "env" +version = "2.0.2" +description = "RT-Thread ENV" +requires-python = ">=3.6" +dependencies = [ + "SCons>=4.0.0", + "requests", + "psutil", + "tqdm", + "kconfiglib", + "pyyaml", + "windows-curses; sys_platform=='win32'", +] + +[project.scripts] +rt-env = "env:main" +menuconfig = "env:menuconfig" +pkgs = "env:pkgs" +sdk = "env:sdk" +system = "env:system" + +[tool.setuptools] +package-dir = {"" = "."} +packages = ["cmds", "cmds.cmd_package"] + [tool.black] line-length = 128 skip-string-normalization = true -# use-tabs = false diff --git a/setup.py b/setup.py index cca760c2..81f93958 100644 --- a/setup.py +++ b/setup.py @@ -1,49 +1,18 @@ from setuptools import setup -from version import get_rt_env_version +import sys +import os + +# Add current directory to path for version module discovery +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + from version import get_rt_env_version + env_name, env_ver = get_rt_env_version() +except Exception: + env_name = 'RT-Thread Env Tool' + env_ver = '2.0.2' -env_name, env_ver = get_rt_env_version() setup( - name='env', version=env_ver, description=env_name, - url='https://github.com/RT-Thread/env.git', - author='RT-Thread Development Team', - author_email='rt-thread@rt-thread.org', - keywords='rt-thread', - license='Apache License 2.0', - project_urls={ - 'Github repository': 'https:/github.com/rt-thread/env.git', - 'User guide': 'https:/github.com/rt-thread/env.git', - }, - python_requires='>=3.6', - install_requires=[ - 'SCons>=4.0.0', - 'requests', - 'psutil', - 'tqdm', - 'kconfiglib', - 'windows-curses; platform_system=="Windows"', - ], - packages=[ - 'env', - 'env.cmds', - 'env.cmds.cmd_package', - ], - package_dir={ - 'env': '.', - 'env.cmds': 'cmds', - 'env.cmds.cmd_package': 'cmds/cmd_package', - }, - package_data={'': ['*.*']}, - exclude_package_data={'': ['MANIFEST.in']}, - include_package_data=True, - entry_points={ - 'console_scripts': [ - 'rt-env=env.env:main', - 'menuconfig=env.env:menuconfig', - 'pkgs=env.env:pkgs', - 'sdk=env.env:sdk', - 'system=env.env:system', - ] - }, ) diff --git a/tools/install.ps1 b/tools/install.ps1 new file mode 100644 index 00000000..1e664e9a --- /dev/null +++ b/tools/install.ps1 @@ -0,0 +1,2227 @@ +# File : install.ps1 +# This file is part of RT-Thread RTOS +# COPYRIGHT (C) 2006 - 2026, RT-Thread Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Change Logs: +# Date Author Notes +# 2026-01-30 dongly Refactored + +# RT-Thread ENV Installation Script (Windows) +# RT-Thread ENV 安装脚本 (Windows) +# Unified installation script for Windows +# Windows 统一安装脚本 +# Supports: English / 中文 +# +# This script handles the initial setup of RT-Thread ENV on Windows. +# 此脚本处理 Windows 上 RT-Thread ENV 的初始设置。 +# It performs steps 1-3 of the installation process: +# 执行安装过程的步骤 1-3: +# 1. Check and install Python and Git - 检查并安装 Python 和 Git +# 2. Enable Windows long path support (requires admin) - 启用 Windows 长路径支持(需要管理员权限) +# 3. Download and execute touch_env.py for steps 4-9 - 下载并执行 touch_env.py 完成步骤 4-9 +# +# Usage: +# 用法: +# .\install.ps1 [-y] [-c] [-o] [-d] [-p [path]] [-r ] [-e|-z] [-P [#]] [-E [#]] [-S [#]] [-b ] [-t ] [-h] +# +# Options: +# 选项: +# -y, --yes, --auto Auto-install without prompts +# 自动安装,无提示 +# -c, --cn, --gitee Use China mirror (Gitee, PyPI TUNA) +# 使用中国镜像(Gitee, PyPI TUNA) +# -o, --official Force use official source +# 强制使用官方源 +# -d, --pyocd Install pyocd for debugging +# 安装 pyocd(用于调试) +# -r, --env-root Set custom install directory +# 设置自定义安装目录 +# -e, --en, --english Force English messages +# 强制显示英文信息 +# -z, --zh, --chinese Force Chinese messages +# 强制显示中文信息 +# -p, --python [path] Force install portable Python, install directory is path (default: D:\Tools\Python) +# 安装便携式 Python, 安装目录为 path(默认:D:\Tools\Python) +# -P, --packages [#] Specify custom packages repository and branch +# 指定 packages 仓库地址和分支 +# 格式: url[#branch] +# -E, --env [#] Specify custom env repository and branch +# 指定 env 仓库地址和分支 +# 格式: url[#branch] +# -S, --sdk [#] Specify custom sdk repository and branch +# 指定 sdk 仓库地址和分支 +# 格式: url[#branch] +# -b, --backup Backup strategy when ENV exists: +# 当 ENV 已存在时的备份策略: +# preserve: Keep .config and local_pkgs, restore and delete backup +# 保留 .config 和 local_pkgs,恢复后删除备份 +# delete_all: Backup then delete everything, no restore +# 备份后删除所有内容,不恢复 +# delete_all_now: Delete everything immediately, no backup +# 立即删除所有内容,不备份 +# backup_all: Keep backup with hardlink restore +# 保留备份,用硬链接恢复本地包 +# -t, --touch-env-url Specify touch_env.py download URL +# 指定 touch_env.py 下载 URL +# -h, --help Show this help message +# 显示此帮助信息 +# + +# ============================================================================ +# Global Constants +# ============================================================================ + +# touch_env.py download URLs +$TOUCH_ENV_URL_GITHUB = "https://raw.githubusercontent.com/RT-Thread/env/master/tools/touch_env.py" +$TOUCH_ENV_URL_GITEE = "https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/touch_env.py" + +# Python Configuration +$PYTHON_VERSION = "3.13.11" +$PYTHON_ARCHIVE = "python-${PYTHON_VERSION}-amd64.zip" +$PYTHON_URL_DEFAULT = "https://www.python.org/ftp/python/$PYTHON_VERSION/$PYTHON_ARCHIVE" +$PYTHON_URL_CN = "https://registry.npmmirror.com/-/binary/python/$PYTHON_VERSION/$PYTHON_ARCHIVE" +$DEFAULT_PYTHON_PATH = "D:\Tools\Python" + +# Git Configuration +$GIT_FALLBACK_VERSION = "v2.52.0.windows.1" +$GIT_FALLBACK_URL = "https://github.com/git-for-windows/git/releases/download/$GIT_FALLBACK_VERSION/Git-${GIT_FALLBACK_VERSION}-64-bit.exe" +$GIT_GITHUB_API_URL = "https://api.github.com/repos/git-for-windows/git/releases/latest" +$GIT_NPMMIRROR_URL = "https://registry.npmmirror.com/-/binary/git-for-windows/" + +# IP Detection Configuration +$IPINFO_URL = "https://ipinfo.io/json" + +# ============================================================================ +# Helper Functions +# ============================================================================ + +# Parse-Arguments function +# Parse command line arguments and return parsed values +function Read-OptionalArg { + param( + [Parameter(Mandatory = $true)] + [string[]]$Arguments, + [Parameter(Mandatory = $true)] + [int]$Index + ) + + if ($Index + 1 -lt $Arguments.Count -and $Arguments[$Index + 1] -notmatch "^-") { + return $Arguments[++$Index] + } + else { + return "" + } +} + +function Parse-Arguments { + param([string[]]$Arguments) + + $result = [PSCustomObject]@{ + AutoMode = $false + HelpMode = $false + CnMode = $false + OfficialMode = $false + PyocdMode = $false + PythonPath = "" + EnMode = $false + ZhMode = $false + BackupStrategy = "" + EnvRoot = "" + CustomPackages = "" + CustomEnv = "" + CustomSdk = "" + TouchEnvUrlValue = "" + } + + for ($i = 0; $i -lt $Arguments.Count; $i++) { + $arg = $Arguments[$i] + switch -CaseSensitive ($arg) { + "-y" { $result.AutoMode = $true } + "--yes" { $result.AutoMode = $true } + "--auto" { $result.AutoMode = $true } + "-h" { $result.HelpMode = $true } + "--help" { $result.HelpMode = $true } + "-c" { $result.CnMode = $true } + "--cn" { $result.CnMode = $true } + "--gitee" { $result.CnMode = $true } + "-o" { $result.OfficialMode = $true } + "--official" { $result.OfficialMode = $true } + "-d" { $result.PyocdMode = $true } + "--pyocd" { $result.PyocdMode = $true } + "-p" { $result.PythonPath = Read-OptionalArg -Arguments $Arguments -Index $i } + "--python" { $result.PythonPath = Read-OptionalArg -Arguments $Arguments -Index $i } + "-E" { $result.CustomEnv = $Arguments[++$i] } + "--env" { $result.CustomEnv = $Arguments[++$i] } + "-e" { $result.EnMode = $true } + "--en" { $result.EnMode = $true } + "--english" { $result.EnMode = $true } + "-z" { $result.ZhMode = $true } + "--zh" { $result.ZhMode = $true } + "--chinese" { $result.ZhMode = $true } + "-r" { $result.EnvRoot = $Arguments[++$i] } + "--env-root" { $result.EnvRoot = $Arguments[++$i] } + "-P" { $result.CustomPackages = $Arguments[++$i] } + "--packages" { $result.CustomPackages = $Arguments[++$i] } + "-S" { $result.CustomSdk = $Arguments[++$i] } + "--sdk" { $result.CustomSdk = $Arguments[++$i] } + "-b" { $result.BackupStrategy = $Arguments[++$i] } + "--backup" { $result.BackupStrategy = $Arguments[++$i] } + "-t" { $result.TouchEnvUrlValue = $Arguments[++$i] } + "--touch-env-url" { $result.TouchEnvUrlValue = $Arguments[++$i] } + } + } + + return $result +} + +# Register-CleanupHandler function +# Register cleanup handler for temporary files +# This ensures temporary files are cleaned up even if the script exits unexpectedly +function Register-CleanupHandler { + try { + Unregister-Event -SourceIdentifier Script.Cleanup -ErrorAction SilentlyContinue + } + catch {} + + $cleanupAction = { + foreach ($tempFile in $script:Config.TempFiles) { + if (Test-Path $tempFile) { + Remove-Item $tempFile -ErrorAction SilentlyContinue + } + } + } + + Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action $cleanupAction | Out-Null +} + +# Add-TempFile function +# Track temporary file for cleanup +# Called whenever a temporary file is created to ensure it gets cleaned up on exit +function Add-TempFile { + param([string]$FilePath) + + if ($script:Config.TempFiles -notcontains $FilePath) { + $script:Config.TempFiles += $FilePath + } +} + +# Get-SystemLanguage function +# Detect system language, returns 'zh' or 'en' +function Get-SystemLanguage { + $locale = [System.Globalization.CultureInfo]::CurrentUICulture.Name + if ($locale -like "*zh*" -or $locale -like "*CN*") { + return "zh" + } + return "en" +} + +# Print-Help function +# Display help information and exit +function Print-Help { + if ($script:Config.LangCurrent -eq "zh") { + Write-Host "RT-Thread ENV 安装程序" + Write-Host "" + Write-Host "用法: .\install.ps1 [选项]" + Write-Host "" + Write-Host "选项:" + Write-Host " -y, --yes, --auto 自动安装,无需提示" + Write-Host " -c, --cn, --gitee 使用中国镜像(Gitee,清华 PyPI)" + Write-Host " -o, --official 强制使用官方源" + Write-Host " -d, --pyocd 安装 pyocd(用于调试)" + Write-Host " -p, --python [path] 安装便携式 Python, 安装目录为 path(默认:D:\Tools\Python)" + Write-Host " -r, --env-root [path] 设置自定义安装目录" + Write-Host " -e, --en, --english 强制显示英文信息" + Write-Host " -z, --zh, --chinese 强制显示中文信息" + Write-Host " -P, --packages [repo] 指定 packages 仓库地址和分支" + Write-Host " 格式: url[#branch]" + Write-Host " -E, --env [repo] 指定 env 仓库地址和分支" + Write-Host " 格式: url[#branch]" + Write-Host " -S, --sdk [repo] 指定 sdk 仓库地址和分支" + Write-Host " 格式: url[#branch]" + Write-Host " -b, --backup [strategy] 备份策略 (preserve/delete_all/delete_all_now/backup_all)" + Write-Host " preserve: 保留 .config 和 local_pkgs,恢复后删除备份" + Write-Host " delete_all: 备份后删除所有内容,不恢复" + Write-Host " delete_all_now: 立即删除所有内容,不备份" + Write-Host " backup_all: 保留备份,用硬链接恢复本地包" + Write-Host " -t, --touch-env-url [url] 指定 touch_env.py 下载 URL" + Write-Host " -h, --help 显示此帮助信息" + Write-Host "" + } + else { + Write-Host "RT-Thread ENV Installation Script" + Write-Host "" + Write-Host "Usage: .\install.ps1 [OPTIONS]" + Write-Host "" + Write-Host "Options:" + Write-Host " -y, --yes, --auto Auto-install without prompts" + Write-Host " -c, --cn, --gitee Use China mirror (Gitee, PyPI TUNA)" + Write-Host " -o, --official Force use official source" + Write-Host " -d, --pyocd Install pyocd for debugging" + Write-Host " -p, --python [path] Force install portable Python, install directory is path (default: D:\Tools\Python)" + Write-Host " -r, --env-root [path] Set custom install directory" + Write-Host " -e, --en, --english Force English messages" + Write-Host " -z, --zh, --chinese Force Chinese messages" + Write-Host " -P, --packages [repo] Specify custom packages repository and branch" + Write-Host " Format: url[#branch]" + Write-Host " -E, --env [repo] Specify custom env repository and branch" + Write-Host " Format: url[#branch]" + Write-Host " -S, --sdk [repo] Specify custom sdk repository and branch" + Write-Host " Format: url[#branch]" + Write-Host " -b, --backup [strategy] Backup strategy (preserve/delete_all/delete_all_now/backup_all)" + Write-Host " preserve: Keep .config and local_pkgs, restore and delete backup" + Write-Host " delete_all: Backup then delete everything, no restore" + Write-Host " delete_all_now: Delete everything immediately, no backup" + Write-Host " backup_all: Keep backup with hardlink restore" + Write-Host " -t, --touch-env-url [url] Specify touch_env.py download URL" + Write-Host " -h, --help Show this help message" + Write-Host "" + } + exit 0 +} + +# Detect-China function +# Detect if user is in China (by IP or timezone) +function Detect-China { + param( + [bool]$LangEn = $false, + [bool]$LangZh = $false + ) + + $use_cn = $false + + try { + $ip_info = Invoke-RestMethod -Uri $IPINFO_URL -Method Get -UseBasicParsing -TimeoutSec 5 + if ($ip_info.country -eq "CN") { + $use_cn = $true + } + } + catch { + } + + if (-not $use_cn) { + try { + $timezone = [System.TimeZoneInfo]::Local.Id + if ($timezone -like "*Shanghai*" -or $timezone -like "*China*" -or $timezone -like "*Beijing*") { + $use_cn = $true + } + } + catch { + } + } + return $use_cn +} + +# Messages +# Centralized message dictionary for easy maintenance and localization +$script:Messages = @{ + en = @{ + banner_title = "RT-Thread ENV Installation" + info = "INFO" + success = "SUCCESS" + warning = "WARNING" + error = "ERROR" + python_version_too_low = "Python version {0} is too old (requires >= 3.6). Installing portable Python..." + installing_portable_python = "Installing portable Python {0}..." + downloading_portable_python = "Downloading portable Python, from: {0}" + python_installed = "Python installed successfully." + python_version_failed = "Failed to get Python version from: {0}" + python_not_found_or_invalid = "Python not found or invalid. Please install Python first." + python_setup_failed = "Python setup failed with code: {0}" + python_ready = "Python ready: {0} (version: {1})" + git_installed = "Git installed successfully." + git_not_found_no_admin = "Git is not installed and you are not an administrator." + restart_required = "Git installed. Please restart your terminal or run the script again." + git_found = "Git found: {0}" + touch_env_failed = "touch_env.py execution failed with code: {0}" + touch_env_downloaded = "touch_env.py downloaded successfully." + downloading_git = "Downloading Git..." + installing_git = "Installing Git..." + fetching_git_from_npmmirror = "Fetching Git version from npmmirror..." + fetching_git_from_github = "Fetching Git version from GitHub API..." + git_version_found = "Git version found: {0}" + npmmirror_fetch_failed = "Failed to fetch Git version from npmmirror, trying GitHub API..." + download_failed = "Download failed: {0}" + github_api_failed = "GitHub API request failed, using fallback version..." + using_fixed_git_version = "Using fixed Git version: {0}" + git_not_found = "Git is not installed. Please install Git first." + admin_required_for_git_install = "Git installation requires administrator privileges. Please run as administrator." + elevation_failed = "Failed to elevate privileges. Please run as administrator." + execution_policy_too_low = "Execution policy is too low. Need to set to RemoteSigned or higher." + admin_required_for_env_config = "Administrator privileges required to configure Windows environment. Please run as administrator." + admin_run_instructions = "Please run the script as administrator to configure Windows environment:" + admin_step_1 = " 1. Right-click PowerShell" + admin_step_2 = " 2. Select 'Run as administrator'" + admin_step_3 = " 3. Run the script again" + status_current_policy = "Current effective execution policy: {0} (scope: {1})" + status_current_longpath = "Current long path support: {0}" + status_new_policy = "New effective execution policy: {0} (scope: {1})" + status_new_longpath = "New long path support: {0}" + status_enabled = "Enabled" + status_disabled = "Disabled" + verified_policy_set = "Verified: {0} is now {1}" + verified_policy_set_exception = "Success (verified despite exception: {0})" + warning_policy_not_set = "Warning: {0} is {1} (expected RemoteSigned)" + long_path_support_required = "Long path support is required. Please run as administrator." + windows_env_adequate = "Windows environment configuration is adequate." + windows_env_set_failed = "Failed to configure Windows environment." + windows_env_initialized = "Windows environment initialized successfully." + install_portable_python = "Install portable Python - Python {0}" + initializing_windows_env = "Initializing Windows environment..." + requesting_elevation = "Requesting administrator privileges: {0}" + multiple_python_found = "Multiple Python installations found:" + select_python = "Found {0} Python installation(s). Default is option {1} (latest). Select [1-{0}], or {2} to install portable Python: " + auto_selected = "Auto-selected Python: {0}" + python_not_found = "Python not found. Please install Python first." + python_path_prompt = "Enter portable Python installation path" + python_path_default = "[default: {0}]" + python_path_invalid = "Error: Path cannot contain {0}" + python_path_no_permission = "Error: No write permission for directory: {0}" + python_path_creating_dir = "Creating directory: {0}" + python_path_directory_exists = "Error: Directory already exists: {0}. Please specify a different path." + extracting_archive = "Extracting archive: {0}" + cleanup_archive = "Cleaning up archive..." + configuring_pth_file = "Configuring .pth file: {0}" + pth_file_configured = ".pth file configured successfully" + python_pth_config_failed = "Failed to configure .pth file" + downloading_touch_env = "Downloading touch_env.py from: {0}" + touch_env_download_failed = "Failed to download touch_env.py: {0}" + ssl_verification_failed = "SSL verification failed, retrying without verification..." + mirror_selection = "Using mirror: {0}" + china_mirror = "China (Gitee, npmmirror)" + official_mirror = "Official (GitHub, PyPI)" + check_list = "Please check:" + check_list_connection = " 1. Your internet connection" + check_list_url = " 2. The URL is correct: {0}" + check_list_alt_url = " 3. Try using -t parameter to specify a different URL" + } + zh = @{ + banner_title = "RT-Thread ENV 安装程序" + info = "信息" + success = "成功" + warning = "警告" + error = "错误" + python_version_too_low = "Python 版本 {0} 过低(需要 >= 3.6)。将安装便携式 Python..." + installing_portable_python = "正在安装便携式 Python {0}..." + downloading_portable_python = "正在下载便携式 Python,自: {0}" + python_installed = "Python 已安装成功。" + python_version_failed = "从 {0} 获取 Python 版本失败" + python_not_found_or_invalid = "未找到 Python 或 Python 无效。请先安装 Python。" + python_setup_failed = "Python 设置失败,错误代码: {0}" + python_ready = "Python 就绪: {0} (版本: {1})" + git_installed = "Git 已安装成功。" + git_not_found_no_admin = "未安装 Git 且您不是管理员。" + restart_required = "Git 已安装。请重启终端或重新运行脚本。" + git_found = "找到 Git: {0}" + touch_env_failed = "touch_env.py 执行失败,错误代码: {0}" + touch_env_downloaded = "touch_env.py 下载成功。" + downloading_git = "正在下载 Git..." + installing_git = "正在安装 Git..." + fetching_git_from_npmmirror = "正在从 npmmirror 获取 Git 版本..." + fetching_git_from_github = "正在从 GitHub API 获取 Git 版本..." + git_version_found = "找到 Git 版本: {0}" + npmmirror_fetch_failed = "从 npmmirror 获取 Git 版本失败,尝试 GitHub API..." + download_failed = "下载失败: {0}" + github_api_failed = "GitHub API 请求失败,使用备选版本..." + using_fixed_git_version = "使用固定 Git 版本: {0}" + git_not_found = "未安装 Git。请先安装 Git。" + admin_required_for_git_install = "Git 安装需要管理员权限。请以管理员身份运行。" + elevation_failed = "提升权限失败。请以管理员身份运行。" + execution_policy_too_low = "执行策略过低。需要设置为 RemoteSigned 或更高。" + admin_required_for_env_config = "配置 Windows 环境需要管理员权限。请以管理员身份运行。" + admin_run_instructions = "请以管理员身份运行脚本来配置 Windows 环境:" + admin_step_1 = " 1. 右键点击 PowerShell" + admin_step_2 = " 2. 选择 '以管理员身份运行'" + admin_step_3 = " 3. 再次运行脚本" + status_current_policy = "当前生效的执行策略: {0}(作用域: {1})" + status_current_longpath = "当前长路径支持: {0}" + status_new_policy = "新的生效执行策略: {0}(作用域: {1})" + status_new_longpath = "新的长路径支持: {0}" + status_enabled = "已启用" + status_disabled = "已禁用" + verified_policy_set = "已验证: {0} 现在是 {1}" + verified_policy_set_exception = "成功(尽管有异常已验证: {0})" + warning_policy_not_set = "警告: {0} 是 {1}(期望为 RemoteSigned)" + long_path_support_required = "需要启用长路径支持。请以管理员身份运行。" + windows_env_adequate = "Windows 环境配置已满足要求。" + windows_env_set_failed = "Windows 环境配置失败。" + windows_env_initialized = "Windows 环境已成功初始化。" + install_portable_python = "安装便携式 Python - Python {0}" + initializing_windows_env = "正在初始化 Windows 环境..." + requesting_elevation = "正在请求管理员权限: {0}" + multiple_python_found = "找到多个 Python 安装:" + select_python = "找到 {0} 个 Python 安装。默认选项为 {1}(最新)。选择 [1-{0}],或输入 {2} 安装便携式 Python: " + auto_selected = "自动选择 Python: {0}" + python_not_found = "未找到 Python。请先安装 Python。" + python_path_prompt = "请输入便携式 Python 安装路径" + python_path_default = "[默认: {0}]" + python_path_invalid = "错误: 路径不能包含 {0}" + python_path_no_permission = "错误: 没有目录的写入权限: {0}" + python_path_creating_dir = "正在创建目录: {0}" + python_path_directory_exists = "错误: 目录已存在: {0}。请指定其他路径。" + extracting_archive = "正在解压存档: {0}" + cleanup_archive = "正在清理存档..." + configuring_pth_file = "正在配置 .pth 文件: {0}" + pth_file_configured = ".pth 文件配置成功" + python_pth_config_failed = "配置 .pth 文件失败" + downloading_touch_env = "正在下载 touch_env.py,自: {0}" + touch_env_download_failed = "下载 touch_env.py 失败: {0}" + ssl_verification_failed = "SSL 验证失败,正在重试(不验证证书)..." + mirror_selection = "使用镜像: {0}" + china_mirror = "中国(Gitee, npmmirror)" + official_mirror = "官方(GitHub, PyPI)" + check_list = "请检查:" + check_list_connection = " 1. 您的网络连接" + check_list_url = " 2. URL 是否正确: {0}" + check_list_alt_url = " 3. 尝试使用 -t 参数指定不同的 URL" + } +} + +# Message functions +# Get-Message: Get localized message +# Write-LogInfo: Output info log (cyan) +# Write-LogSuccess: Output success log (green) +# Write-LogWarning: Output warning log (yellow) +# Write-LogError: Output error log (red) + +function Get-Message { + param([string]$Key) + + $lang = $script:Config.LangCurrent + if ($script:Messages.ContainsKey($lang) -and $script:Messages[$lang].ContainsKey($Key)) { + return $script:Messages[$lang][$Key] + } + + return "Unknown message: $Key" +} + +function Write-LogInfo { + param([string]$Key, [string]$Arg1, [string]$Arg2) + $msg = Get-Message $Key + $formatted = if ($null -ne $Arg1 -and $null -ne $Arg2) { + $msg -f $Arg1, $Arg2 + } + elseif ($null -ne $Arg1) { + $msg -f $Arg1 + } + else { + $msg + } + Write-Host "[$(Get-Message 'info')] $formatted" -ForegroundColor Cyan +} + +function Write-LogSuccess { + param([string]$Key, [string]$Arg1, [string]$Arg2) + $msg = Get-Message $Key + $formatted = if ($null -ne $Arg1 -and $null -ne $Arg2) { + $msg -f $Arg1, $Arg2 + } + elseif ($null -ne $Arg1) { + $msg -f $Arg1 + } + else { + $msg + } + Write-Host "[$(Get-Message 'success')] $formatted" -ForegroundColor Green +} + +function Write-LogWarning { + param([string]$Key, [string]$Arg1, [string]$Arg2) + $msg = Get-Message $Key + $formatted = if ($null -ne $Arg1 -and $null -ne $Arg2) { + $msg -f $Arg1, $Arg2 + } + elseif ($null -ne $Arg1) { + $msg -f $Arg1 + } + else { + $msg + } + Write-Host "[$(Get-Message 'warning')] $formatted" -ForegroundColor Yellow +} + +function Write-LogError { + param([string]$Key, [string]$Arg1, [string]$Arg2) + $msg = Get-Message $Key + $formatted = if ($null -ne $Arg1 -and $null -ne $Arg2) { + $msg -f $Arg1, $Arg2 + } + elseif ($null -ne $Arg1) { + $msg -f $Arg1 + } + else { + $msg + } + Write-Host "[$(Get-Message 'error')] $formatted" -ForegroundColor Red +} + +# Write-LogRaw function +# Write raw message with configurable color and i18n support +function Write-LogRaw { + param( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $false)] + [ConsoleColor]$Color = "White", + + [Parameter(Mandatory = $false)] + [string]$Arg1 = "", + + [Parameter(Mandatory = $false)] + [string]$Arg2 = "" + ) + + # Get message from dictionary (supports i18n) + $msg = if ($script:Messages.ContainsKey($script:Config.LangCurrent) -and $script:Messages[$script:Config.LangCurrent].ContainsKey($Key)) { + $script:Messages[$script:Config.LangCurrent][$Key] + } + else { + $Key # Fallback to the key itself if not found in dictionary + } + + # Format message with arguments if provided + $formatted = if ($null -ne $Arg1 -and $null -ne $Arg2 -and $Arg1 -ne "" -and $Arg2 -ne "") { + $msg -f $Arg1, $Arg2 + } + elseif ($null -ne $Arg1 -and $Arg1 -ne "") { + $msg -f $Arg1 + } + else { + $msg + } + + Write-Host $formatted -ForegroundColor $Color +} + +# Git Functions +# Test-Command: Test if command exists + +function Test-Command { + param([string]$CommandName) + + try { + Get-Command $CommandName -ErrorAction Stop | Out-Null + return $true + } + catch { + return $false + } +} + +# Git Installation Functions +# Get-LatestGitVersion: Get latest Git version +# Install-Git: Install Git (Windows) + +function Get-LatestGitVersion { + param([bool]$UseCNMirror) + + # Helper function to filter valid versions + function Test-ValidGitVersion { + param([string]$Name) + return ($Name -notmatch "-rc\d+" -and + $Name -notmatch "-prerelease$" -and + $Name -notmatch "-mingit$") + } + + # Helper function to build result object + function New-GitVersionInfo { + param( + [string]$Version, + [string]$Installer, + [string]$Url, + [string]$Source + ) + return @{ + Version = $Version + Installer = $Installer + Url = $Url + Source = $Source + } + } + + # Try npmmirror first if using CN mirror + if ($UseCNMirror) { + try { + Write-LogInfo "fetching_git_from_npmmirror" + $versions = Invoke-RestMethod -Uri $GIT_NPMMIRROR_URL -Method Get -UseBasicParsing | + Where-Object { Test-ValidGitVersion -Name $_.name } | + Sort-Object -Property Name -Descending + + if ($versions.Count -gt 0) { + $versionNumber = $versions[0].name -replace '/$', '' + $versionUrl = "$GIT_NPMMIRROR_URL$versionNumber/" + $versionFiles = Invoke-RestMethod -Uri $versionUrl -Method Get -UseBasicParsing + $installerFile = $versionFiles | Where-Object { $_.name -match "^Git-\d+\.\d+\.\d+-64-bit\.exe$" } + + if ($installerFile) { + Write-LogSuccess "git_version_found" "$versionNumber (from npmmirror)" + return New-GitVersionInfo -Version $versionNumber -Installer $installerFile.name -Url $installerFile.url -Source "npmmirror" + } + } + } + catch { + Write-LogWarning "npmmirror_fetch_failed" + } + } + + # Fallback to GitHub API + try { + Write-LogInfo "fetching_git_from_github" + $response = Invoke-RestMethod -Uri $GIT_GITHUB_API_URL -Method Get -UseBasicParsing -ErrorAction Stop + + if ($response -is [string]) { + throw "Received HTML instead of JSON" + } + + $versionNumber = $response.tag_name -replace '^v', '' + $installerAsset = $response.assets | Where-Object { $_.name -match "^Git-\d+\.\d+\.\d+-64-bit\.exe$" } + + if ($installerAsset) { + Write-LogSuccess "git_version_found" "$versionNumber (from GitHub)" + return New-GitVersionInfo -Version $versionNumber -Installer $installerAsset.name -Url $installerAsset.browser_download_url -Source "github" + } + } + catch { + Write-LogWarning "github_api_failed" + } + + # Ultimate fallback: use fixed version + Write-LogWarning "using_fixed_git_version" "$GIT_FALLBACK_VERSION" + return New-GitVersionInfo -Version $GIT_FALLBACK_VERSION -Installer "Git-$GIT_FALLBACK_VERSION-64-bit.exe" -Url $GIT_FALLBACK_URL -Source "fallback" +} + +function Install-Git { + param( + [bool]$UseCNMirror, + [bool]$Interactive = $false + ) + + # Get latest Git version dynamically + $gitInfo = Get-LatestGitVersion -UseCNMirror $UseCNMirror + + Write-LogInfo "downloading_git" + + $installerPath = Join-Path $env:TEMP $gitInfo.Installer + $gitUrl = $gitInfo.Url + + # Track temporary file for cleanup + Add-TempFile -FilePath $installerPath + + try { + # Download Git installer + Invoke-WebRequest -Uri $gitUrl -OutFile $installerPath -UseBasicParsing -ErrorAction Stop + + Write-LogInfo "installing_git" + + if ($Interactive) { + # Interactive installation - show installer UI with default options + Start-Process -FilePath $installerPath -Wait + } + else { + # Silent installation with progress display + # /SILENT: Silent installation with progress bar + # /SUPPRESSMSGBOXES: Suppress message boxes + # /NORESTART: Prevent restart + # /COMPONENTS="": Install all components + # /TASKS="desktopicon,winterminal": Add desktop icon and Windows Terminal profile + # /MERGETASKS="desktopicon,winterminal": Additional tasks to merge + # /DEFAULTBRANCH="main": Set default branch name to main + Start-Process -FilePath $installerPath -ArgumentList @( + "/SILENT", + "/SUPPRESSMSGBOXES", + "/NORESTART", + "/COMPONENTS=", + '/TASKS="desktopicon,winterminal"', + "/DEFAULTBRANCH=main" + ) -Wait + } + + Write-LogSuccess "git_installed" + } + catch { + Write-LogError "download_failed" $_.Exception.Message + throw + } + finally { + # Cleanup installer + Remove-Item $installerPath -ErrorAction SilentlyContinue + } +} + +# Python Installation Functions +# Install-Python: Install portable Python +# Download-PortablePython: Download portable Python +# Extract-PortablePython: Extract portable Python +# Configure-PythonPth: Configure Python _pth file + +class PythonConfig { + [bool]$InstallPortablePython + [string]$PythonPath + [string]$Version + [int]$Result +} +function New-PythonConfig { + return [PythonConfig] @{ + InstallPortablePython = $false + PythonPath = "" + Version = "" + Result = 0 + } +} + +function New-PortingPythonConfig { + param( + [Parameter(Mandatory = $false)] + [string]$PythonPath = $null + ) + + # Use provided PythonPath if available, otherwise fallback to config + if ([string]::IsNullOrEmpty($PythonPath)) { + $PythonPath = $script:Config.PythonConfig.PythonPath + } + + # Check if PythonPath is empty, prompt user if needed + if ([string]::IsNullOrEmpty($PythonPath)) { + if (-not $script:Config.AutoMode) { + $promptedPath = Prompt-PythonPath + if ($promptedPath) { + $PythonPath = $promptedPath + $script:Config.PythonConfig.PythonPath = $promptedPath + } + else { + # Use default if prompt failed + $PythonPath = Join-Path $DEFAULT_PYTHON_PATH "python.exe" + $script:Config.PythonConfig.PythonPath = $PythonPath + } + } + else { + # Auto mode: use default + $PythonPath = Join-Path $DEFAULT_PYTHON_PATH "python.exe" + $script:Config.PythonConfig.PythonPath = $PythonPath + + # Check if directory already exists + $pythonTargetDir = Split-Path -Parent $PythonPath + if (Test-Path $pythonTargetDir) { + Write-LogError "python_path_directory_exists" $pythonTargetDir + return [PythonConfig] @{ + InstallPortablePython = $false + PythonPath = "" + Version = "" + Result = 1 + } + } + } + } + + return [PythonConfig] @{ + InstallPortablePython = $true + PythonPath = $PythonPath + Version = $PYTHON_VERSION + Result = 0 + } +} + +function Prompt-PythonPath { + # Prompt user to enter portable Python installation path + # Returns full path with python.exe suffix + param() + + # Only work in non-auto mode + if ($script:Config.AutoMode) { + return "" + } + + $pythonPath = "" + $isValid = $false + + while (-not $isValid) { + # Display prompt with default value + $promptMsg = Get-Message "python_path_prompt" + $defaultMsgRaw = Get-Message "python_path_default" + $defaultMsg = $defaultMsgRaw -f $DEFAULT_PYTHON_PATH + Write-Host "$promptMsg $defaultMsg" -NoNewline -ForegroundColor Yellow + $input = Read-Host + + # Use default if input is empty + if ([string]::IsNullOrWhiteSpace($input)) { + $input = $DEFAULT_PYTHON_PATH + } + + # Check if directory already exists + if (Test-Path $input) { + Write-LogError "python_path_directory_exists" $input + continue + } + + # Check path format (spaces, non-ASCII characters) + if ($input -match "\s") { + Write-LogError "python_path_invalid" "spaces" + continue + } + if ($input -match "[^\x00-\x7F]") { + Write-LogError "python_path_invalid" "non-ASCII characters" + continue + } + + # Check parent directory and create if needed + $parentDir = Split-Path -Parent $input + if (-not (Test-Path $parentDir)) { + Write-LogInfo "python_path_creating_dir" $parentDir + try { + New-Item -ItemType Directory -Path $parentDir -Force | Out-Null + } + catch { + Write-LogError "python_path_no_permission" $parentDir + continue + } + } + + # Check write permission + $testFile = Join-Path $parentDir ".__write_test__" + try { + [System.IO.File]::WriteAllText($testFile, "test") + Remove-Item $testFile -Force -ErrorAction SilentlyContinue + } + catch { + Write-LogError "python_path_no_permission" $parentDir + continue + } + + # Path is valid + $isValid = $true + $pythonPath = Join-Path $input "python.exe" + } + + return $pythonPath +} + +function Check-Python { + param( + [Parameter(Mandatory = $true)] + [PythonConfig]$PythonConfig + ) + + $result = New-PortingPythonConfig -PythonPath $PythonConfig.PythonPath + # Check if Python.exe exists + if (-not (Test-Path $PythonConfig.PythonPath)) { + Write-LogError "python_not_found" $PythonConfig.PythonPath + $result.Result = 1 + return $result + } + + # Get Python version + $version = Get-PythonVersionString -PythonPath $PythonConfig.PythonPath + if (-not $version) { + Write-LogWarning "python_version_failed" $PythonConfig.PythonPath + $result.Result = 2 + return $result + } + $PythonConfig.Version = $version + + # Check if version meets minimum requirement (>= 3.6) + if (-not (Test-PythonVersion -VersionString $version)) { + Write-LogError "python_version_too_low" $version + $result.Result = 3 + return $result + } + + # All checks passed + return $PythonConfig +} + +function Download-PortablePython { + param([bool]$UseCNMirror) + + # Determine download URL based on mirror setting + $pythonUrl = if ($UseCNMirror) { $PYTHON_URL_CN } else { $PYTHON_URL_DEFAULT } + + Write-LogInfo "downloading_portable_python" $pythonUrl + $archivePath = Join-Path $env:TEMP $PYTHON_ARCHIVE + + # Track temporary file for cleanup + Add-TempFile -FilePath $archivePath + + # Download Python embed archive + try { + Invoke-WebRequest -Uri $pythonUrl -OutFile $archivePath -UseBasicParsing -ErrorAction Stop + } + catch { + Write-LogError "download_failed" $_.Exception.Message + exit 1 + } + + # Verify file was downloaded successfully + if (-not (Test-Path $archivePath) -or (Get-Item $archivePath).Length -eq 0) { + Write-LogError "download_failed" "File not found or empty" + exit 1 + } +} + +function Extract-PortablePython { + Write-LogInfo "installing_portable_python" $PYTHON_VERSION + + $archivePath = Join-Path $env:TEMP $PYTHON_ARCHIVE + # Extract directory from PythonConfig.PythonPath (which includes python.exe) + $pythonTargetDir = Split-Path -Parent $script:Config.PythonConfig.PythonPath + + # Check if directory already exists + if (Test-Path $pythonTargetDir) { + Write-LogError "python_path_directory_exists" $pythonTargetDir + exit 1 + } + + # Create directory + Write-LogInfo "python_path_creating_dir" $pythonTargetDir + New-Item -ItemType Directory -Path $pythonTargetDir -Force | Out-Null + + # Extract zip file, excluding Doc directory + try { + Add-Type -AssemblyName System.IO.Compression.FileSystem + Add-Type -AssemblyName System.IO.Compression + Write-LogInfo "extracting_archive" $archivePath + $zip = [System.IO.Compression.ZipFile]::OpenRead($archivePath) + try { + $totalEntries = ($zip.Entries | Where-Object { $_.FullName -notlike 'Doc/*' -and $_.FullName -ne 'Doc' }).Count + $current = 0 + foreach ($entry in $zip.Entries) { + if ($entry.FullName -like 'Doc/*' -or $entry.FullName -eq 'Doc') { continue } + $current++ + if ($current % 10 -eq 0 -or $current -eq $totalEntries) { + Write-Progress -Activity "Extracting Python" -Status "File $current of $totalEntries" -PercentComplete (($current / $totalEntries) * 100) -Id 1 + } + $entryPath = Join-Path $pythonTargetDir $entry.FullName + if ($entry.Name -eq '') { + # Directory entry + New-Item -ItemType Directory -Path $entryPath -Force | Out-Null + } + else { + $entryDir = Split-Path $entryPath -Parent + if (-not (Test-Path $entryDir)) { + New-Item -ItemType Directory -Path $entryDir -Force | Out-Null + } + # Use .NET 4.5+ method to extract file + $stream = [System.IO.File]::Create($entryPath) + try { + $entryStream = $entry.Open() + try { + $entryStream.CopyTo($stream) + } + finally { + $entryStream.Dispose() + } + } + finally { + $stream.Dispose() + } + } + } + Write-Progress -Activity "Extracting Python" -Completed -Id 1 + } + finally { + $zip.Dispose() + } + } + catch { + Write-Host " [错误详情] $_" -ForegroundColor Red + Write-Host " [错误位置] $($_.ScriptStackTrace)" -ForegroundColor Red + exit 1 + } + + # Cleanup archive + Write-LogInfo "cleanup_archive" + Remove-Item $archivePath -ErrorAction SilentlyContinue +} + + + +function Configure-PythonPth { + # Modify python3xx._pth to enable site-packages and ensurepip + # Extract directory from PythonConfig.PythonPath (which includes python.exe) + $pythonTargetDir = Split-Path -Parent $script:Config.PythonConfig.PythonPath + Write-LogInfo "configuring_pth_file" $pythonTargetDir + try { + $pthFile = Get-ChildItem -Path $pythonTargetDir -Filter "*._pth" -ErrorAction Stop + if ($pthFile) { + $pthContent = Get-Content -Path $pthFile.FullName -Raw -ErrorAction Stop + # Uncomment import site to enable site-packages + $pthContent = $pthContent -replace "#import site", "import site" + Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline -ErrorAction Stop + } + } + catch { + Write-LogWarning "python_pth_config_failed" + } +} + +function Install-PortablePython { + param( + [bool]$UseCNMirror + ) + + $result = New-PythonConfig + + try { + # Download and extract portable Python + Download-PortablePython -UseCNMirror $UseCNMirror + Extract-PortablePython + # Configure python3xx._pth file + Configure-PythonPth + # Save portable Python path to global variable + $portablePython = $script:Config.PythonConfig.PythonPath + Write-LogSuccess "python_installed" + + # Get Python version + $version = Get-PythonVersionString -PythonPath $portablePython + if ($version) { + $result.PythonPath = $portablePython + $result.Version = $version + $result.InstallPortablePython = $true + $result.Result = 0 + } + else { + Write-LogWarning "python_version_failed" $portablePython + $result.Result = 1 + } + } + catch { + $result.Result = 2 + Write-LogError "python_install_failed" $_.Exception.Message + Write-Host "请手动删除 Python 安装目录后重试" -ForegroundColor Yellow + } + + return $result +} + +# Python Environment Setup Functions +# Find-SystemPython: Find system Python +# Find-LatestPythonVersion: Find latest Python version +# Show-PythonOptions: Show Python options +# Handle-PythonSelection: Handle Python selection +# Select-Python: Select Python installation +# Get-PythonVersionString: Get Python version string +# Test-PythonVersion: Test if Python version meets requirements + +function Find-SystemPython { + # Build search paths + $searchPaths = @( + "$env:LOCALAPPDATA\Programs\Python\python.exe" + "$env:LOCALAPPDATA\Programs\Python\Python*\python.exe" + "$env:ProgramFiles\Python\python.exe" + "${env:ProgramFiles(x86)}\Python\python.exe" + "$env:ProgramFiles\Python\Python*\python.exe" + "${env:ProgramFiles(x86)}\Python\Python*\python.exe" + "$env:USERPROFILE\Anaconda3\python.exe" + "$env:USERPROFILE\Miniconda3\python.exe" + "$env:USERPROFILE\conda\python.exe" + ) + + # Add drive-specific paths + foreach ($drive in (Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Root)) { + # $searchPaths += "$drive\Python*\python.exe" + $searchPaths += "$drive\py*\python.exe" + $searchPaths += "$drive\Tools\Python*\python.exe" + $searchPaths += "$drive\Anaconda3\python.exe" + $searchPaths += "$drive\Miniconda3\python.exe" + } + + # Test if a path is a valid Python executable + function Test-PythonPath { + param([string]$Path) + + try { + # Check if it's a command name (not a full path) + if ($Path -notmatch '[\\/]') { + # For command names, use Get-Command to resolve + $cmdInfo = Get-Command -Name $Path -ErrorAction SilentlyContinue + if ($cmdInfo) { + $actualPath = $cmdInfo.Source + # Skip Windows Store Python launcher + if ($actualPath -like '*WindowsApps\python.exe') { + return $false + } + # Test the actual path + $version = & $actualPath --version 2>&1 + return $version -match "Python" + } + return $false + } + else { + # For full paths, test directly + $version = & $Path --version 2>&1 + return $version -match "Python" + } + } + catch { + return $false + } + } + + $foundPaths = @() + + # Search all paths + foreach ($pythonPath in $searchPaths) { + if ($pythonPath -like '*\*') { + # Handle wildcard paths + try { + $resolvedPaths = Resolve-Path -Path $pythonPath -ErrorAction SilentlyContinue + if ($resolvedPaths) { + foreach ($resolvedPath in $resolvedPaths) { + if (Test-PythonPath -Path $resolvedPath.Path) { + $foundPaths += $resolvedPath.Path + } + } + } + } + catch { + continue + } + } + elseif (Test-Path $pythonPath -and (Test-PythonPath -Path $pythonPath)) { + $foundPaths += $pythonPath + } + } + + # Check system PATH commands (py launcher) + foreach ($cmd in @("py")) { + if (Test-PythonPath -Path $cmd) { + $foundPaths += $cmd + } + } + + # Remove duplicates + return @($foundPaths | Select-Object -Unique) +} + +function Find-LatestPythonVersion { + param( + [Parameter(Mandatory = $true)] + [string[]]$PythonPaths + ) + + $latestPython = $null + $latestVersion = [version]"0.0.0" + $latestIndex = 0 + + for ($i = 0; $i -lt $PythonPaths.Count; $i++) { + $verString = & $PythonPaths[$i] --version 2>&1 | Select-String "Python" + $verString = $verString.Line -replace 'Python ', '' + try { + $currentVersion = [version]$verString + if ($currentVersion -gt $latestVersion) { + $latestVersion = $currentVersion + $latestPython = $PythonPaths[$i] + $latestIndex = $i + } + } + catch { + # If version parsing fails, use this Python if we haven't found one yet + if (-not $latestPython) { + $latestPython = $PythonPaths[$i] + $latestIndex = $i + } + continue + } + } + + # Fallback: if no Python was selected (all version parsing failed), use the first one + if (-not $latestPython) { + $latestPython = $PythonPaths[0] + $latestIndex = 0 + } + + return @{ + Python = $latestPython + Index = $latestIndex + } +} + +function Show-PythonOptions { + param( + [Parameter(Mandatory = $true)] + [string[]]$PythonPaths + ) + + Write-Host "" + Write-LogInfo "multiple_python_found" + + for ($i = 0; $i -lt $PythonPaths.Count; $i++) { + $ver = & $PythonPaths[$i] --version 2>&1 | Select-String "Python" + Write-Host " $($i + 1)). $($PythonPaths[$i]) - $($ver.Line)" + } + $portablePythonMsg = Get-Message 'install_portable_python' + $portablePythonMsg = $portablePythonMsg -replace '\{0\}', $script:PYTHON_VERSION + Write-Host " $($PythonPaths.Count + 1)). $portablePythonMsg" -ForegroundColor Cyan + Write-Host "" +} + +function Handle-PythonSelection { + param( + [Parameter(Mandatory = $true)] + [string[]]$PythonPaths, + [Parameter(Mandatory = $true)] + [int]$LatestIndex + ) + + $result = New-PythonConfig + $result.InstallPortablePython = $false + + $msg = Get-Message "select_python" + $formatted = $msg -f $PythonPaths.Count, ($LatestIndex + 1), ($PythonPaths.Count + 1) + Write-Host $formatted -NoNewline -ForegroundColor Yellow + $choice = Read-Host + + if ([string]::IsNullOrEmpty($choice)) { + # Use default (latest) + $result.PythonPath = $PythonPaths[$LatestIndex] + } + else { + try { + $choiceInt = [int]$choice + } + catch { + # Invalid input (non-numeric), use default + Write-LogWarning "python_not_found" "" + $result.PythonPath = $PythonPaths[$LatestIndex] + return $result + } + + if ($choiceInt -ge 1 -and $choiceInt -le $PythonPaths.Count) { + $result.PythonPath = $PythonPaths[$choiceInt - 1] + } + elseif ($choiceInt -eq ($PythonPaths.Count + 1)) { + # Install portable Python + # Check if PythonPath is empty, prompt user if in non-auto mode + if ([string]::IsNullOrEmpty($script:Config.PythonConfig.PythonPath)) { + $promptedPath = Prompt-PythonPath + if ($promptedPath) { + $script:Config.PythonConfig.PythonPath = $promptedPath + } + } + $result = New-PortingPythonConfig + } + else { + # Invalid choice, use default + $result.PythonPath = $PythonPaths[$LatestIndex] + } + } + return $result +} + +function Select-Python { + param( + [Parameter(Mandatory = $true)] + [string[]]$PythonPaths, + [bool]$SkipVerification = $false + ) + + $result = $Script:Config.PythonConfig + + if ($PythonPaths.Count -eq 0) { + return $result + } + + # Find the latest version to use as default + $latestInfo = Find-LatestPythonVersion -PythonPaths $PythonPaths + $latestPython = $latestInfo.Python + $latestIndex = $latestInfo.Index + $result.InstallPortablePython = $false + + # In auto mode, automatically select the latest version + if ($script:Config.AutoMode) { + $msg = Get-Message "auto_selected" + $formatted = $msg -f $latestPython + Write-Host $formatted -ForegroundColor Yellow + $result.PythonPath = $latestPython + # Get version and validate + $version = Get-PythonVersionString -PythonPath $latestPython + if ($version -and (Test-PythonVersion -VersionString $version)) { + $result.Version = $version + $result.Result = 0 + } + else { + $result = New-PortingPythonConfig -PythonPath $latestPython + } + } + else { + # Interactive mode, let user choose + Show-PythonOptions -PythonPaths $PythonPaths + $result = Handle-PythonSelection -PythonPaths $PythonPaths -LatestIndex $latestIndex + } + + return $result +} +function Get-PythonVersionString { + param([Parameter(Mandatory = $true)][string]$PythonPath) + + try { + $version = & $PythonPath --version 2>&1 | Select-String "Python" + if ($?) { + return $version.Line -replace 'Python ', '' + } + } + catch { + return $null + } + return $null +} + +# Test-PythonVersion function +# Test if Python version meets minimum requirement (>= 3.6) +function Test-PythonVersion { + param([Parameter(Mandatory = $true)][string]$VersionString) + + $versionParts = $VersionString -split '[ .]' + if ($versionParts.Count -ge 2) { + $major = [int]$versionParts[0] + $minor = [int]$versionParts[1] + if ($major -gt 3 -or ($major -eq 3 -and $minor -ge 6)) { + return $true + } + } + return $false +} +# UI Functions +# Show-Banner: Display installation banner + +function Show-Banner { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host " $(Get-Message 'banner_title') " -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" +} + +# Installation Process Functions + +function Ensure-Python { + $result = $script:Config.PythonConfig + + # 步骤 0: 检查便携 Python 路径是否为空 + if ($result.InstallPortablePython -and [string]::IsNullOrEmpty($result.PythonPath)) { + if ($script:Config.AutoMode) { + # Auto mode: use default path + $result.PythonPath = Join-Path $DEFAULT_PYTHON_PATH "python.exe" + } + else { + # Interactive mode: prompt user for path + $promptedPath = Prompt-PythonPath + if ($promptedPath) { + $result.PythonPath = $promptedPath + $script:Config.PythonConfig.PythonPath = $promptedPath + } + else { + # User cancelled or invalid input, use default + $result.PythonPath = Join-Path $DEFAULT_PYTHON_PATH "python.exe" + $script:Config.PythonConfig.PythonPath = $result.PythonPath + } + } + } + + # 步骤 1: 查找选择系统 Python + if (-not $result.InstallPortablePython) { + $result = Select-Python -PythonPaths (Find-SystemPython) + } + + # 步骤 2: 验证系统 Python + if (-not $result.InstallPortablePython -and $result.PythonPath) { + $result = Check-Python -PythonConfig $result + } + if ($result.InstallPortablePython ) { + if ($result.Result -eq 0) { + Write-LogWarning "installing_portable_python" + } + else { + Write-LogWarning "python_not_found_or_invalid" + } + } + + # 步骤 3: 安装便携式 Python(如果需要) + if ($result.InstallPortablePython) { + $result = Install-PortablePython -UseCNMirror $script:Config.UseCN + } + + # 步骤 4: 检查结果 + if ($result.Result -ne 0) { + Write-LogError "python_setup_failed" $result.Result + exit $result.Result + } + $Script:Config.PythonConfig = $result + Write-LogSuccess "python_ready" $result.PythonPath $result.Version +} + +function Ensure-Git { + # Check and install Git if missing + if (-not (Test-Command "git")) { + Write-LogInfo "git_not_found" + if (-not $script:Config.IsAdmin) { + Write-LogError "git_not_found_no_admin" + Write-LogWarning "admin_required_for_git_install" + exit 1 + } + # When -y is used, install Git silently; otherwise show interactive installer + Install-Git -UseCNMirror $script:Config.UseCN -Interactive (-not $script:Config.AutoMode) + Write-Host "" + Write-LogWarning "restart_required" + Read-Host -Prompt "Press Enter to exit..." + exit 0 + } + + $gitVersion = git --version 2>&1 + $gitVersion = $gitVersion.Trim() + Write-LogSuccess "git_found" $gitVersion +} + +# Save-TouchEnvToFile function +# Save touch_env.py script content to temporary file +function Save-TouchEnvToFile { + param( + [string]$ScriptContent + ) + + $touchEnvTempFile = Join-Path $env:TEMP "touch_env.py" + Add-TempFile -FilePath $touchEnvTempFile + Set-Content -Path $touchEnvTempFile -Value $ScriptContent -Encoding UTF8 + return $touchEnvTempFile +} + +# Build-TouchEnvArgs function +# Build argument list for touch_env.py +function Build-TouchEnvArgs { + param( + [string]$TouchEnvFilePath + ) + + # Build arguments list + $pythonArgs = @($TouchEnvFilePath) + # 条件传递 --env-root + if ($script:Config.EnvRoot) { + $pythonArgs += "--env-root", $script:Config.EnvRoot + } + if ($script:Config.UseCN) { $pythonArgs += "--use-cn" } + $pythonArgs += "--language", $script:Config.LangCurrent + if ($script:Config.AutoMode) { $pythonArgs += "--auto-mode" } + if ($script:Config.InstallPyocd) { $pythonArgs += "--install-pyocd" } + + # Pass custom repositories with branch info in URL fragment + if ($script:Config.CustomEnv) { + $pythonArgs += "--repo-env", $script:Config.CustomEnv + } + + if ($script:Config.CustomPackages) { + $pythonArgs += "--repo-packages", $script:Config.CustomPackages + } + + if ($script:Config.CustomSdk) { + $pythonArgs += "--repo-sdk", $script:Config.CustomSdk + } + + # Pass backup strategy + if ($script:Config.BackupStrategy) { + $pythonArgs += "--backup", $script:Config.BackupStrategy + } + + return $pythonArgs +} + +# Show-TouchEnvError function +# Show touch_env.py error output if available +function Show-TouchEnvError { + $errorFile = "$env:TEMP\touch_env_error.txt" + if (Test-Path $errorFile) { + $errorOutput = Get-Content $errorFile -Raw + if ($errorOutput) { + Write-Host $errorOutput -ForegroundColor Red + } + } +} + +# Invoke-TouchEnv function +# Download and execute touch_env.py to handle Step 5-10 +function Invoke-TouchEnv { + param( + [string]$ScriptContent + ) + + try { + # Save touch_env.py to temp file + $touchEnvFile = Save-TouchEnvToFile -ScriptContent $ScriptContent + + # Build arguments list + $pythonArgs = Build-TouchEnvArgs -TouchEnvFilePath $touchEnvFile + + # 显示"$env:TEMP\touch_env_output.txt" 的内容 + Write-Host "运行参数: $pythonArgs" -ForegroundColor Green + + # Run touch_env.py in the same window with full interactivity + & $script:Config.PythonConfig.PythonPath $pythonArgs + $touchEnvExitCode = $LASTEXITCODE + + if ($touchEnvExitCode -ne 0) { + Write-LogError "touch_env_failed" $touchEnvExitCode + Show-TouchEnvError + exit $touchEnvExitCode + } + } + catch { + Write-LogError "touch_env_download_failed" $_.Exception.Message + exit 1 + } +} + + + +# ============================================================================ +# Windows Environment Initialization Functions +# ============================================================================ + + + +# Request-Elevation: Request administrator privileges for a task +function Request-Elevation { + param( + [string]$TaskDescription, + [string]$ScriptBlock + ) + + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if ($isAdmin) { + return $true + } + + # Create temporary script + $tempScript = [System.IO.Path]::GetTempFileName() + ".ps1" + Add-TempFile -FilePath $tempScript + + if ($ScriptBlock) { + $ScriptBlock | Out-File -FilePath $tempScript -Encoding UTF8 + } + else { + "" | Out-File -FilePath $tempScript -Encoding UTF8 + } + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "powershell.exe" + $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -NoExit -File `"$tempScript`"" + $psi.Verb = "RunAs" + $psi.UseShellExecute = $true + + try { + Write-LogInfo "requesting_elevation" $TaskDescription + $process = [System.Diagnostics.Process]::Start($psi) + $process.WaitForExit() + return $process.ExitCode -eq 0 + } + catch { + Write-LogWarning "elevation_failed" $TaskDescription + return $false + } +} + +# Get-EffectiveExecutionPolicy: Get the effective execution policy (excluding Process scope) +# Returns a hashtable with Policy and EffectiveScope +function Get-EffectiveExecutionPolicy { + $policyLevels = @{ + "Undefined" = 0 + "Restricted" = 1 + "AllSigned" = 2 + "RemoteSigned" = 3 + "Unrestricted" = 4 + "Bypass" = 5 + } + + try { + # Get all execution policies + $policies = Get-ExecutionPolicy -List -ErrorAction SilentlyContinue + + # If policies is empty or null, Get-ExecutionPolicy failed + if (-not $policies -or $policies.Count -eq 0) { + # Fallback: try to get each scope individually + $fallbackPolicies = @() + foreach ($scope in @("MachinePolicy", "UserPolicy", "Process", "CurrentUser", "LocalMachine")) { + try { + $policy = Get-ExecutionPolicy -Scope $scope -ErrorAction SilentlyContinue + $fallbackPolicies += [PSCustomObject]@{ + Scope = $scope + ExecutionPolicy = $policy + } + } catch { + $fallbackPolicies += [PSCustomObject]@{ + Scope = $scope + ExecutionPolicy = "Undefined" + } + } + } + $policies = $fallbackPolicies + } + + # Priority order: MachinePolicy > UserPolicy > Process > CurrentUser > LocalMachine + # We exclude Process scope as it's temporary + $scopePriority = @("MachinePolicy", "UserPolicy", "CurrentUser", "LocalMachine") + + foreach ($scope in $scopePriority) { + $policy = $policies | Where-Object { $_.Scope -eq $scope } + if ($policy -and $policy.ExecutionPolicy -ne "Undefined") { + return @{ + Policy = $policy.ExecutionPolicy + EffectiveScope = $scope + } + } + } + + # If all are Undefined, return Restricted (default) + return @{ + Policy = "Restricted" + EffectiveScope = "LocalMachine (default)" + } + } + catch { + # If Get-ExecutionPolicy fails, assume Restricted + return @{ + Policy = "Restricted" + EffectiveScope = "Unknown" + } + } +} + +# Show-CurrentExecutionPolicyStatus: Display current execution policy status +function Show-CurrentExecutionPolicyStatus { + param( + [hashtable]$PolicyInfo + ) + + $currentPolicy = $PolicyInfo.Policy + $currentScope = $PolicyInfo.EffectiveScope + + if ($currentScope) { + $statusMsg = Get-Message "status_current_policy" + $formatted = $statusMsg -f $currentPolicy, $currentScope + Write-Host $formatted -ForegroundColor Cyan + } else { + $statusMsg = Get-Message "status_current_policy" + $formatted = $statusMsg -f $currentPolicy, "N/A" + Write-Host $formatted -ForegroundColor Cyan + } +} + +# Check-ExecutionPolicy: Check if execution policy needs to be changed +function Check-ExecutionPolicy { + param( + [hashtable]$PolicyInfo + ) + + $currentPolicy = $PolicyInfo.Policy + $currentScope = $PolicyInfo.EffectiveScope + + $policyLevels = @{ + "Undefined" = 0 + "Restricted" = 1 + "AllSigned" = 2 + "RemoteSigned" = 3 + "Unrestricted" = 4 + "Bypass" = 5 + } + + $currentLevel = $policyLevels[$currentPolicy.ToString()] + $targetLevel = $policyLevels["RemoteSigned"] + $needPolicy = ($null -eq $currentLevel -or $currentLevel -lt $targetLevel) + + # Determine which scope to set based on effective scope + $scopeToSet = "" + if ($needPolicy) { + # Extract scope name from "Scope (description)" format if needed + if ($currentScope -match "^(.*?)\s*\(") { + $scopeName = $Matches[1].Trim() + } else { + $scopeName = $currentScope + } + $scopeToSet = if ($scopeName -and $scopeName -ne "Process" -and $scopeName -ne "Unknown" -and $scopeName -ne "N/A") { $scopeName } else { "LocalMachine" } + $script:Config.ScopeToSet = $scopeToSet + } + + return @{ + NeedPolicy = $needPolicy + ScopeToSet = $scopeToSet + } +} + +# Check-LongPathSupport: Check if long path support needs to be enabled +function Check-LongPathSupport { + $needLongPath = $false + $longPathStatus = "Unknown" + + try { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" + $longPathEnabled = (Get-ItemProperty -Path $registryPath -ErrorAction SilentlyContinue).LongPathsEnabled + $needLongPath = ($longPathEnabled -ne 1) + $longPathStatusKey = if ($longPathEnabled -eq 1) { "status_enabled" } else { "status_disabled" } + $longPathStatus = Get-Message $longPathStatusKey + Write-LogRaw "status_current_longpath" -Color Cyan -Arg1 $longPathStatus + } + catch { + $needLongPath = $true + Write-Host "Current long path support: Unknown (assuming Disabled)" -ForegroundColor Yellow + } + + return @{ + NeedLongPath = $needLongPath + CurrentStatus = $longPathStatus + } +} + +# Show-ConfigurationNeeds: Display what needs to be changed +function Show-ConfigurationNeeds { + param( + [bool]$NeedPolicy, + [bool]$NeedLongPath + ) + + if ($NeedPolicy) { + Write-LogWarning "execution_policy_too_low" + } + if ($NeedLongPath) { + Write-LogWarning "long_path_support_required" + } +} + +# Execute-SetExecutionPolicy: Execute Set-ExecutionPolicy command +function Execute-SetExecutionPolicy { + param( + [string]$Scope + ) + + $action = "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope $Scope -Force" + Write-Host "Executing: $action" -ForegroundColor Yellow + + $output = Invoke-Expression $action 2>&1 + $actualSuccess = $true + + # Get current Process scope policy to check if it's the effective one + $processPolicy = try { + Get-ExecutionPolicy -Scope Process -ErrorAction SilentlyContinue + } catch { + "Undefined" + } + + # Check if the output contains the "overridden by a policy" warning + if ($output -match "overridden by a policy" -and $processPolicy -eq "Bypass") { + # Only ignore if Process scope is currently effective (Bypass) + Write-Host "Success (policy overridden by Process scope: $processPolicy)" -ForegroundColor Green + } elseif ($LASTEXITCODE -ne 0) { + $actualSuccess = $false + Write-LogWarning "windows_env_set_failed" + Write-Host "Error: $output" -ForegroundColor Red + } else { + Write-Host "Success" -ForegroundColor Green + } + + return $actualSuccess +} + +# Verify-ExecutionPolicy: Verify execution policy was set correctly +function Verify-ExecutionPolicy { + param( + [string]$Scope + ) + + $actualPolicy = try { + Get-ExecutionPolicy -Scope $Scope -ErrorAction SilentlyContinue + } catch { + $null + } + + if ($actualPolicy -eq "RemoteSigned") { + Write-LogRaw "verified_policy_set" -Color Green -Arg1 $Scope -Arg2 $actualPolicy + return $true + } else { + Write-LogWarning "windows_env_set_failed" + Write-LogRaw "warning_policy_not_set" -Color Yellow -Arg1 $Scope -Arg2 $actualPolicy + return $false + } +} + +# Show-NewPolicyStatus: Display new effective policy status +function Show-NewPolicyStatus { + $newPolicyInfo = Get-EffectiveExecutionPolicy + $newEffectivePolicy = $newPolicyInfo.Policy + $newEffectiveScope = $newPolicyInfo.EffectiveScope + + if ($newEffectiveScope) { + $statusMsg = Get-Message "status_new_policy" + $formatted = $statusMsg -f $newEffectivePolicy, $newEffectiveScope + Write-Host $formatted -ForegroundColor Green + } else { + $statusMsg = Get-Message "status_new_policy" + $formatted = $statusMsg -f $newEffectivePolicy, "N/A" + Write-Host $formatted -ForegroundColor Green + } +} + +# Execute-EnableLongPath: Execute command to enable long path support +function Execute-EnableLongPath { + $action = 'Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWord -Force' + Write-Host "Executing: $action" -ForegroundColor Yellow + + $output = Invoke-Expression $action 2>&1 + $actualSuccess = $true + + if ($LASTEXITCODE -ne 0) { + $actualSuccess = $false + Write-LogWarning "windows_env_set_failed" + Write-Host "Error: $output" -ForegroundColor Red + } else { + Write-Host "Success" -ForegroundColor Green + } + + return $actualSuccess +} + +# Verify-LongPathSupport: Verify long path support was enabled +function Verify-LongPathSupport { + $newLongPathEnabled = try { + (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -ErrorAction SilentlyContinue).LongPathsEnabled + } catch { + 0 + } + + $newLongPathStatusKey = if ($newLongPathEnabled -eq 1) { "status_enabled" } else { "status_disabled" } + $newLongPathStatus = Get-Message $newLongPathStatusKey + Write-LogRaw "status_new_longpath" -Color Green -Arg1 $newLongPathStatus + + return ($newLongPathEnabled -eq 1) +} + +# Configure-WindowsEnvironment: Apply Windows environment configuration changes +function Configure-WindowsEnvironment { + param( + [bool]$NeedPolicy, + [bool]$NeedLongPath + ) + + $actions = @() + $allSuccess = $true + + if ($NeedPolicy) { + $scope = $script:Config.ScopeToSet + $actions += @{ + command = "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope $scope -Force" + type = "policy" + } + } + + if ($NeedLongPath) { + $actions += @{ + command = 'Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWord -Force' + type = "longpath" + } + } + + foreach ($actionItem in $actions) { + $action = $actionItem.command + $actionType = $actionItem.type + + try { + if ($actionType -eq "policy") { + $success = Execute-SetExecutionPolicy -Scope $script:Config.ScopeToSet + if ($success) { + Verify-ExecutionPolicy -Scope $script:Config.ScopeToSet + Show-NewPolicyStatus + } else { + $allSuccess = $false + } + } elseif ($actionType -eq "longpath") { + $success = Execute-EnableLongPath + if ($success) { + Verify-LongPathSupport + } else { + $allSuccess = $false + } + } + } + catch { + # Even if exception occurs, check if the policy was actually set + if ($action -match "Set-ExecutionPolicy") { + $scope = $script:Config.ScopeToSet + $actualPolicy = try { + Get-ExecutionPolicy -Scope $scope -ErrorAction SilentlyContinue + } catch { + $null + } + if ($actualPolicy -eq "RemoteSigned") { + $exceptionMsg = $_.Exception.Message + Write-LogRaw "verified_policy_set_exception" -Color Green -Arg1 $exceptionMsg + Write-LogRaw "verified_policy_set" -Color Green -Arg1 $scope -Arg2 $actualPolicy + Show-NewPolicyStatus + } else { + Write-LogError "windows_env_set_failed" + Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red + $allSuccess = $false + } + } else { + Write-LogError "windows_env_set_failed" + Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red + $allSuccess = $false + } + } + } + + return $allSuccess +} + +# Init-WindowsEnv: Initialize Windows environment settings +function Init-WindowsEnv { + Write-LogInfo "initializing_windows_env" + + # Check execution policy + $currentPolicyInfo = Get-EffectiveExecutionPolicy + Show-CurrentExecutionPolicyStatus -PolicyInfo $currentPolicyInfo + + $policyCheck = Check-ExecutionPolicy -PolicyInfo $currentPolicyInfo + + # Check long path support + $longPathCheck = Check-LongPathSupport + + # If everything is OK, return + if (-not $policyCheck.NeedPolicy -and -not $longPathCheck.NeedLongPath) { + Write-LogSuccess "windows_env_adequate" + return + } + + # Show what needs to be changed + Show-ConfigurationNeeds -NeedPolicy $policyCheck.NeedPolicy -NeedLongPath $longPathCheck.NeedLongPath + + # Check if running as administrator + if ($script:Config.IsAdmin) { + $success = Configure-WindowsEnvironment -NeedPolicy $policyCheck.NeedPolicy -NeedLongPath $longPathCheck.NeedLongPath + + if ($success) { + Write-LogSuccess "windows_env_initialized" + } else { + exit 1 + } + return + } else { + # Not admin, show error and exit + Write-LogError "admin_required_for_env_config" + Write-Host "" + Write-LogRaw "admin_run_instructions" -Color Yellow + Write-LogRaw "admin_step_1" -Color White + Write-LogRaw "admin_step_2" -Color White + Write-LogRaw "admin_step_3" -Color White + exit 1 + } +} + + +# Init-Config function +# Initialize installation environment and validate settings +function Init-Config { + param( + [PSCustomObject]$ParsedArg + ) + + # Set strict mode and error handling + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + # Initialize global config + $script:Config = [PSCustomObject]@{ + LangCurrent = "" + UseCN = $false + UseCNSet = $false + InstallPyocd = $false + AutoMode = $false + NeedHelp = $false + BackupStrategy = "" + IsAdmin = $false + CustomPackages = "" + CustomEnv = "" + CustomSdk = "" + PythonConfig = New-PythonConfig + EnvRoot = "" + TempFiles = @() + ScopeToSet = "" + } + + # Register cleanup handler (must be after Config initialization) + Register-CleanupHandler + + # Set config from parsed arguments + $script:Config.LangCurrent = if ($ParsedArgs.ZhMode) { "zh" } elseif ($ParsedArgs.EnMode) { "en" } else { Get-SystemLanguage } + $script:Config.UseCN = $ParsedArgs.CnMode + $script:Config.UseCNSet = $ParsedArgs.CnMode -or $ParsedArgs.OfficialMode + $script:Config.InstallPyocd = $ParsedArgs.PyocdMode + $script:Config.AutoMode = $ParsedArgs.AutoMode + $script:Config.NeedHelp = $ParsedArgs.HelpMode + $script:Config.BackupStrategy = $ParsedArgs.BackupStrategy + $script:Config.CustomPackages = $ParsedArgs.CustomPackages + $script:Config.CustomEnv = $ParsedArgs.CustomEnv + $script:Config.CustomSdk = $ParsedArgs.CustomSdk + + # Set PythonConfig.PythonPath based on command line arguments + if ($ParsedArgs.PythonPath) { + # User specified path via -p parameter + $script:Config.PythonConfig.PythonPath = Join-Path $ParsedArgs.PythonPath "python.exe" + } + # else: PythonConfig.PythonPath remains empty (will be prompted later) + + # Set EnvRoot for passing to touch_env.py + $script:Config.EnvRoot = $ParsedArgs.EnvRoot + + # Check administrator privileges + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + $script:Config.IsAdmin = $isAdmin + + # Handle help request + if ($script:Config.NeedHelp) { + Print-Help + return + } + + # Detect China mirror if not explicitly set + if (-not $script:Config.UseCNSet) { + $script:Config.UseCN = Detect-China + } + $mirrorType = if ($script:Config.UseCN) { "china_mirror" } else { "official_mirror" } + Write-LogInfo "mirror_selection" (Get-Message $mirrorType) + + # Override with --official flag + if ($ParsedArgs.OfficialMode) { + $script:Config.UseCN = $false + } +} + +# Show-DownloadError function +# Show download error message with checklist +function Show-DownloadError { + param( + [string]$Url + ) + + Write-Host "" + Write-LogRaw "check_list" -Color Yellow + Write-LogRaw "check_list_connection" -Color Yellow + Write-LogRaw "check_list_url" -Color Yellow -Arg1 $Url + Write-LogRaw "check_list_alt_url" -Color Yellow +} + +# Download-TouchEnv function +# Download touch_env.py script from network with fallback handling +function Download-TouchEnv { + param( + [PSCustomObject]$ParsedArgs + ) + + # Set touch_env.py download URL (priority: -t > --env > UseCN/Gitee > GitHub) + if ($ParsedArgs.TouchEnvUrlValue) { + $TOUCH_ENV_URL = $ParsedArgs.TouchEnvUrlValue + } + elseif ($ParsedArgs.CustomEnv) { + # Use custom env repo for touch_env.py download + # Parse URL and branch from string (format: url[#branch]) + if ($ParsedArgs.CustomEnv -match "#") { + $parts = $ParsedArgs.CustomEnv -split "#", 2 + $repo = $parts[0] + $branch = $parts[1] + } + else { + $repo = $ParsedArgs.CustomEnv + $branch = "master" + } + + # Convert GitHub repo URL to raw.githubusercontent.com URL + if ($repo -match "^https?://github\.com/([^/]+)/([^/]+?)(\.git)?$") { + # GitHub repository: https://github.com/owner/repo -> https://raw.githubusercontent.com/owner/repo/branch/tools/touch_env.py + $owner = $Matches[1] + $repoName = $Matches[2] -replace '\.git$', '' + $TOUCH_ENV_URL = "https://raw.githubusercontent.com/$owner/$repoName/$branch/tools/touch_env.py" + } + else { + # Non-GitHub repository: use /raw/ format + $TOUCH_ENV_URL = "$repo/raw/$branch/tools/touch_env.py" + } + } + elseif ($script:Config.UseCN) { + $TOUCH_ENV_URL = $TOUCH_ENV_URL_GITEE + } + else { + $TOUCH_ENV_URL = $TOUCH_ENV_URL_GITHUB + } + + # Download touch_env.py from network + Write-Host "" + Write-LogInfo "downloading_touch_env" $TOUCH_ENV_URL + $scriptContent = $null + + try { + # Try with SSL verification first + $response = Invoke-WebRequest -Uri $TOUCH_ENV_URL -UseBasicParsing -ErrorAction Stop + $scriptContent = $response.Content + Write-LogInfo "touch_env_downloaded" + } + catch { + # If SSL error, try without SSL verification + if ($_.Exception.Message -match "SSL" -or $_.Exception.Message -match "certificate") { + Write-LogWarning "ssl_verification_failed" + try { + $response = Invoke-WebRequest -Uri $TOUCH_ENV_URL -UseBasicParsing -SkipCertificateCheck -ErrorAction Stop + $scriptContent = $response.Content + } + catch { + Write-LogError "touch_env_download_failed" $_.Exception.Message + Show-DownloadError -Url $TOUCH_ENV_URL + exit 1 + } + } + else { + Write-LogError "touch_env_download_failed" $_.Exception.Message + Show-DownloadError -Url $TOUCH_ENV_URL + exit 1 + } + } + + return $scriptContent +} + +# Main Function +# Main function: Coordinate all installation steps +function Main { + # Parse command line arguments + $parsedArgs = Parse-Arguments -Arguments $args + + # Initialize configuration + Init-Config -ParsedArgs $parsedArgs + + # Initialize Windows environment + Init-WindowsEnv + + # Step 1: Print installation banner + Show-Banner + + # Step 2: Ensure Python and Git are installed + Ensure-Python + Ensure-Git + + # Step 3: Download touch_env.py + $scriptContent = Download-TouchEnv -ParsedArgs $parsedArgs + + # Step 4: Call touch_env.py to handle Step 5-10 + Invoke-TouchEnv -ScriptContent $scriptContent +} + +# Execute main function +Main @args diff --git a/tools/install.sh b/tools/install.sh new file mode 100755 index 00000000..7abbef7e --- /dev/null +++ b/tools/install.sh @@ -0,0 +1,752 @@ +#!/usr/bin/env bash +# +# File : install.sh +# This file is part of RT-Thread RTOS +# COPYRIGHT (C) 2006 - 2026, RT-Thread Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Change Logs: +# Date Author Notes +# 2026-01-31 dongly Refactored + +# RT-Thread ENV Installation Script (Unix) +# Unified installation script for Linux and macOS +# Supports: English / 中文 +# +# Usage: +# ./install.sh [-y] [-c] [-o] [-d] [-r ] [-e|-z] [-P [#]] [-E [#]] [-S [#]] [-b ] [-t ] [-h] +# +# Options: +# -y, --yes, --auto Auto-install without prompts +# -c, --cn, --gitee Use China mirror (Gitee, PyPI TUNA) +# -o, --official Force use official source +# -d, --pyocd Install pyocd for debugging +# -r, --env-root Set custom install directory +# -e, --en, --english Force English messages +# -z, --zh, --chinese Force Chinese messages +# -P, --packages [#] Specify custom packages repository and branch +# -E, --env [#] Specify custom env repository and branch +# -S, --sdk [#] Specify custom sdk repository and branch +# -b, --backup Backup strategy when ENV exists: +# preserve: Keep .config and local_pkgs, restore and delete backup +# 保留 .config 和 local_pkgs,恢复后删除备份 +# delete_all: Backup then delete everything, no restore +# 备份后删除所有内容,不恢复 +# delete_all_now: Delete everything immediately, no backup +# 立即删除所有内容,不备份 +# backup_all: Keep backup with hardlink restore +# 保留备份,用硬链接恢复工具链 +# -t, --touch-env-url Specify touch_env.py download URL +# -h, --help Show this help message +# + +# ============================================================================ +# Configuration +# ============================================================================ + +# Verify script is running in bash or zsh +if [ -z "$BASH_VERSION" ] && [ -z "$ZSH_VERSION" ]; then + echo "Error: This script must be run with bash or zsh, not sh" >&2 + exit 1 +fi + +# Global configuration variables (like $script:Config in PowerShell) +CONFIG_AUTO_MODE=false +CONFIG_HELP_MODE=false +CONFIG_PYOCD_MODE=false +CONFIG_ENV_ROOT="" +CONFIG_LANG="en" +CONFIG_USE_CN_SET=false +CONFIG_USE_CN=false +CONFIG_CUSTOM_PACKAGES_REPO="" +CONFIG_CUSTOM_ENV_REPO="" +CONFIG_CUSTOM_SDK_REPO="" +CONFIG_BACKUP_STRATEGY="" +CONFIG_TOUCH_ENV_URL_VALUE="" + +# Global variables for user context (initialized in init_environment) +REAL_USER_HOME="" +REAL_USER="" +REAL_USER_SHELL="" + +# Global variable for tracking temporary files (for cleanup) +TEMP_FILES=() + +# IP detection service +IPINFO_URL="https://ipinfo.io/json" + +# Homebrew installation script +HOMEBREW_INSTALL_URL="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + +# touch_env.py download URLs +TOUCH_ENV_URL_GITHUB="https://raw.githubusercontent.com/RT-Thread/env/master/tools/touch_env.py" +TOUCH_ENV_URL_GITEE="https://gitee.com/RT-Thread-Mirror/env/raw/master/tools/touch_env.py" + +# ============================================================================ +# Message Dictionary (Centralized i18n messages like PowerShell $script:Messages) +# ============================================================================ + +declare -A MESSAGES_EN=( + ["banner_title"]="RT-Thread ENV Installation" + ["info"]="INFO" + ["success"]="SUCCESS" + ["warning"]="WARNING" + ["error"]="ERROR" + ["git_not_found"]="Git is not installed. Please install Git first." + ["git_found"]="Git version: %s" + ["start"]="Starting RT-Thread ENV installation..." + ["installing_ubuntu"]="Installing dependencies (Ubuntu/Debian)..." + ["installing_suse"]="Installing dependencies (SUSE/openSUSE)..." + ["installing_arch"]="Installing dependencies (Arch/Manjaro)..." + ["installing_fedora"]="Installing dependencies (Fedora/RHEL/CentOS)..." + ["installing_alpine"]="Installing dependencies (Alpine)..." + ["unsupported_os"]="Unsupported OS: %s" + ["missing_gcc"]="Missing GCC compiler, please install manually" + ["installing_homebrew"]="Installing Homebrew..." + ["installing_macos"]="Installing dependencies (macOS)..." + ["missing_python"]="Python 3 not found. Please install Python first." + ["python_version"]="Python version: %s" + ["using_cn_mirror"]="Using China mirror" + ["using_official_source"]="Using official source" + ["downloading_touch_env"]="Downloading touch_env.py from: %s" + ["touch_env_downloaded"]="touch_env.py downloaded successfully." + ["touch_env_failed"]="touch_env.py execution failed with exit code: %s" + ["touch_env_download_failed"]="Failed to download touch_env.py: %s" +) + +declare -A MESSAGES_ZH=( + ["banner_title"]="RT-Thread ENV 安装程序" + ["info"]="信息" + ["success"]="成功" + ["warning"]="警告" + ["error"]="错误" + ["git_not_found"]="未安装 Git。请先安装 Git。" + ["git_found"]="Git 版本: %s" + ["start"]="开始启动 RT-Thread ENV 安装..." + ["installing_ubuntu"]="正在安装依赖 (Ubuntu/Debian)..." + ["installing_suse"]="正在安装依赖 (SUSE/openSUSE)..." + ["installing_arch"]="正在安装依赖 (Arch/Manjaro)..." + ["installing_fedora"]="正在安装依赖 (Fedora/RHEL/CentOS)..." + ["installing_alpine"]="正在安装依赖 (Alpine)..." + ["unsupported_os"]="不支持的操作系统: %s" + ["missing_gcc"]="缺少 GCC 编译器,请手动安装" + ["installing_homebrew"]="正在安装 Homebrew..." + ["installing_macos"]="正在安装依赖 (macOS)..." + ["missing_python"]="未找到 Python 3,请先安装" + ["python_version"]="Python 版本: %s" + ["using_cn_mirror"]="使用中国镜像源" + ["using_official_source"]="使用官方源" + ["downloading_touch_env"]="正在下载 touch_env.py,自: %s" + ["touch_env_downloaded"]="touch_env.py 下载完成。" + ["touch_env_failed"]="touch_env.py 执行失败,退出码: %s" + ["touch_env_download_failed"]="下载 touch_env.py 失败: %s" +) + +# ============================================================================ +# Initialization Functions +# ============================================================================ + +init_environment() { + # Get real user's home directory (handles sudo case) + if [ -n "$SUDO_USER" ]; then + # Running with sudo, use the original user's home + REAL_USER_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) + REAL_USER="$SUDO_USER" + # Get the real user's default shell from /etc/passwd + REAL_USER_SHELL=$(getent passwd "$SUDO_USER" | cut -d: -f7) + else + # Running without sudo + REAL_USER_HOME="$HOME" + REAL_USER="$USER" + REAL_USER_SHELL="$SHELL" + fi + + # Detect language based on IP or system locale + detect_china +} + +# Cleanup function for temporary files +cleanup() { + for temp_file in "${TEMP_FILES[@]}"; do + rm -f "$temp_file" 2>/dev/null + done +} + +# Register cleanup handler for exit signals +trap cleanup EXIT INT TERM + +# ============================================================================ +# Message Functions +# ============================================================================ + +# Get message from dictionary (similar to PowerShell Get-Message) +get_message() { + local key="$1" + + # Select appropriate language dictionary + if [ "$CONFIG_LANG" = "zh" ]; then + if [ -n "${MESSAGES_ZH[$key]+isset}" ]; then + echo "${MESSAGES_ZH[$key]}" + else + echo "Unknown message: $key" + fi + else + if [ -n "${MESSAGES_EN[$key]+isset}" ]; then + echo "${MESSAGES_EN[$key]}" + else + echo "Unknown message: $key" + fi + fi +} + +# Log functions (similar to PowerShell Write-LogInfo/Success/Warning/Error) +log_info() { + local key="$1" + shift + local msg + msg=$(get_message "$key") + + # Format message with arguments + if [ $# -gt 0 ]; then + # shellcheck disable=SC2059 + printf "\033[0;34m[%s]\033[0m ${msg}\n" "$(get_message 'info')" "$@" >&2 + else + printf "\033[0;34m[%s]\033[0m ${msg}\n" "$(get_message 'info')" >&2 + fi +} + +log_success() { + local key="$1" + shift + local msg + msg=$(get_message "$key") + + if [ $# -gt 0 ]; then + # shellcheck disable=SC2059 + printf "\033[0;32m[%s]\033[0m ${msg}\n" "$(get_message 'success')" "$@" >&2 + else + printf "\033[0;32m[%s]\033[0m ${msg}\n" "$(get_message 'success')" >&2 + fi +} + +log_warning() { + local key="$1" + shift + local msg + msg=$(get_message "$key") + + if [ $# -gt 0 ]; then + # shellcheck disable=SC2059 + printf "\033[1;33m[%s]\033[0m ${msg}\n" "$(get_message 'warning')" "$@" >&2 + else + printf "\033[1;33m[%s]\033[0m ${msg}\n" "$(get_message 'warning')" >&2 + fi +} + +log_error() { + local key="$1" + shift + local msg + msg=$(get_message "$key") + + if [ $# -gt 0 ]; then + # shellcheck disable=SC2059 + printf "\033[0;31m[%s]\033[0m ${msg}\n" "$(get_message 'error')" "$@" >&2 + else + printf "\033[0;31m[%s]\033[0m ${msg}\n" "$(get_message 'error')" >&2 + fi +} + +# ============================================================================ +# Download and Execute touch_env.py Functions +# ============================================================================ + +download_and_run_touch_env() { + # Create temp file + local touch_env_dest + touch_env_dest=$(mktemp --suffix=.py) + + # Track temp file for cleanup + TEMP_FILES+=("$touch_env_dest") + + # Download touch_env.py (determines URL internally) + download_touch_env "$touch_env_dest" || { + return 1 + } + + # Run touch_env.py + run_touch_env "$touch_env_dest" || { + return 1 + } + + # Temp file will be cleaned up by trap handler + + return 0 +} + +download_touch_env() { + local touch_env_dest="$1" + + # Determine touch_env.py download URL + local touch_env_download_url="$TOUCH_ENV_URL_GITHUB" + + if [ -n "$CONFIG_TOUCH_ENV_URL_VALUE" ]; then + touch_env_download_url="$CONFIG_TOUCH_ENV_URL_VALUE" + elif [ -n "$CONFIG_CUSTOM_ENV_REPO" ]; then + # Parse URL and branch from string (format: url[#branch]) + local repo="$CONFIG_CUSTOM_ENV_REPO" + local branch="master" + if [[ "$repo" == *"#"* ]]; then + branch="${repo#*#}" + repo="${repo%#*}" + fi + + # Convert GitHub repo URL to raw.githubusercontent.com URL + if [[ "$repo" =~ ^https?://github\.com/([^/]+)/([^/]+?)(\.git)?$ ]]; then + local owner="${BASH_REMATCH[1]}" + local repo_name="${BASH_REMATCH[2]%.git}" + touch_env_download_url="https://raw.githubusercontent.com/$owner/$repo_name/$branch/tools/touch_env.py" + else + # Non-GitHub repository: use /raw/ format + touch_env_download_url="$repo/raw/$branch/tools/touch_env.py" + fi + elif [ "$CONFIG_USE_CN" = "true" ]; then + touch_env_download_url="$TOUCH_ENV_URL_GITEE" + fi + + log_info "downloading_touch_env" "$touch_env_download_url" + + # Download touch_env.py + if command -v curl &> /dev/null 2>&1; then + curl -fsSL --connect-timeout 30 "$touch_env_download_url" -o "$touch_env_dest" + elif command -v wget &> /dev/null 2>&1; then + wget --timeout=30 -O "$touch_env_dest" "$touch_env_download_url" + else + log_error "touch_env_download_failed" "$touch_env_download_url" + return 1 + fi + + if [ ! -s "$touch_env_dest" ]; then + log_error "touch_env_download_failed" "$touch_env_download_url" + return 1 + fi + + log_success "touch_env_downloaded" +} + +run_touch_env() { + local touch_env_dest="$1" + + # Build Python command arguments for touch_env.py + local python_args=() + + if [ -n "$CONFIG_ENV_ROOT" ]; then + python_args+=("--env-root" "$CONFIG_ENV_ROOT") + fi + + if [ "$CONFIG_USE_CN" = "true" ]; then + python_args+=("--use-cn") + fi + + if [ "$CONFIG_LANG" = "en" ]; then + python_args+=("--language" "en") + elif [ "$CONFIG_LANG" = "zh" ]; then + python_args+=("--language" "zh") + fi + + if [ "$CONFIG_AUTO_MODE" = "true" ]; then + python_args+=("--auto-mode") + fi + + if [ -n "$CONFIG_BACKUP_STRATEGY" ]; then + python_args+=("--backup" "$CONFIG_BACKUP_STRATEGY") + fi + + if [ "$CONFIG_PYOCD_MODE" = "true" ]; then + python_args+=("--install-pyocd") + fi + + # Custom repositories (pass full URL, touch_env.py parses branch if present) + if [ -n "$CONFIG_CUSTOM_PACKAGES_REPO" ]; then + python_args+=("--repo-packages" "$CONFIG_CUSTOM_PACKAGES_REPO") + fi + + if [ -n "$CONFIG_CUSTOM_ENV_REPO" ]; then + python_args+=("--repo-env" "$CONFIG_CUSTOM_ENV_REPO") + fi + + if [ -n "$CONFIG_CUSTOM_SDK_REPO" ]; then + python_args+=("--repo-sdk" "$CONFIG_CUSTOM_SDK_REPO") + fi + + log_info "start" + + # Execute touch_env.py as REAL_USER + if [ -n "$SUDO_USER" ]; then + su "$REAL_USER" -c "python3 '$touch_env_dest' ${python_args[*]}" + else + python3 "$touch_env_dest" "${python_args[@]}" + fi + + local result=$? + + if [ $result -ne 0 ]; then + log_error "touch_env_failed" "$result" + return 1 + fi + + return 0 +} + +# ============================================================================ +# Argument Parsing +# ============================================================================ + +print_help() { + echo "$(get_message 'banner_title')" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -y, --yes, --auto Auto-install without prompts" + echo " -c, --cn, --gitee Use China mirror (Gitee, PyPI TUNA)" + echo " -o, --official Force use official source" + echo " -d, --pyocd Install pyocd for debugging" + echo " -r, --env-root Set custom install directory" + echo " -e, --en, --english Force English messages" + echo " -z, --zh, --chinese Force Chinese messages" + echo " -P, --packages [#] Specify custom packages repository and branch" + echo " -E, --env [#] Specify custom env repository and branch" + echo " -S, --sdk [#] Specify custom sdk repository and branch" + echo " -b, --backup Backup strategy (preserve/delete_all/delete_all_now/backup_all)" + echo " preserve: Keep .config and local_pkgs, restore and delete backup" + echo " 保留 .config 和 local_pkgs,恢复后删除备份" + echo " delete_all: Backup then delete everything, no restore" + echo " 备份后删除所有内容,不恢复" + echo " delete_all_now: Delete everything immediately, no backup" + echo " 立即删除所有内容,不备份" + echo " backup_all: Keep backup with hardlink restore" + echo " 保留备份,用硬链接恢复工具链" + echo " -t, --touch-env-url Specify touch_env.py download URL" + echo " -h, --help Show this help message" + echo "" +} + +check_git() { + if ! command -v git &> /dev/null; then + log_error "git_not_found" + return 1 + fi + local git_version + git_version=$(git --version 2>&1 | grep -E 'git version' | awk '{print $3}') + log_info "git_found" "$git_version" + return 0 +} + +detect_china() { + # Check if user is in China (by IP or system locale) + # Only set CONFIG_USE_CN, don't override CONFIG_LANG (which may be set by --en/--zh) + if [ "$CONFIG_USE_CN_SET" = "true" ]; then + return # User explicitly set mirror, skip detection + fi + + # Check IP-based detection (works on all systems) + if command -v curl &> /dev/null 2>&1; then + local ip_info=$(curl -s -m 5 --connect-timeout 3 "$IPINFO_URL" 2>&1) + if [[ "$ip_info" == *"\"country\":\"CN\""* ]]; then + CONFIG_USE_CN="true" + return + fi + fi + + # Fallback: check system timezone + local timezone=$(date +%Z 2>/dev/null || timedatectl show -p Timezone --value 2>/dev/null || echo "") + if [[ "$timezone" == *"CST"* ]] || [[ "$timezone" == *"Shanghai"* ]] || [[ "$timezone" == *"Beijing"* ]] || [[ "$timezone" == *"Asia/Shanghai"* ]]; then + CONFIG_USE_CN="true" + return + fi + + # Fallback: check system locale - only set CONFIG_USE_CN + case "${LC_ALL}:${LANG}" in + *zh*|*CN*) + CONFIG_USE_CN="true" + ;; + esac +} + +parse_args() { + # Local state variables for parse_args (not global config) + CONFIG_LANG_SET="false" + CONFIG_OFFICIAL_MODE="false" + CONFIG_CUSTOM_PACKAGES_BRANCH="" + CONFIG_CUSTOM_ENV_BRANCH="" + CONFIG_CUSTOM_SDK_BRANCH="" + + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + CONFIG_HELP_MODE="true" + ;; + -y|--yes|--auto) + CONFIG_AUTO_MODE="true" + ;; + -e|--en|--english) + CONFIG_EN_MODE="true" + CONFIG_LANG="en" + CONFIG_LANG_SET="true" + ;; + -z|--zh|--chinese) + CONFIG_ZH_MODE="true" + CONFIG_LANG="zh" + CONFIG_LANG_SET="true" + ;; + -r|--env-root) + shift + CONFIG_ENV_ROOT="$1" + ENV_ROOT="$1" + ;; + -P|--packages) + shift + CONFIG_CUSTOM_PACKAGES_REPO="$1" + ;; + -E|--env) + shift + CONFIG_CUSTOM_ENV_REPO="$1" + ;; + -S|--sdk) + shift + CONFIG_CUSTOM_SDK_REPO="$1" + ;; + -c|--cn|--gitee) + CONFIG_CN_MODE="true" + CONFIG_USE_CN_SET="true" + CONFIG_USE_CN="true" + CONFIG_LANG="zh" + ;; + -o|--official) + CONFIG_OFFICIAL_MODE="true" + CONFIG_USE_CN_SET="true" + ;; + -d|--pyocd) + CONFIG_PYOCD_MODE="true" + ;; + -b|--backup) + shift + CONFIG_BACKUP_STRATEGY="$1" + ;; + -t|--touch-env-url) + shift + CONFIG_TOUCH_ENV_URL_VALUE="$1" + ;; + *) + # Unknown argument, skip + ;; + esac + shift + done + + # IP detection (lower priority, only if not explicitly set) + if [ "$CONFIG_USE_CN_SET" = "false" ]; then + detect_china + fi + + # Override with --official flag + if [ "$CONFIG_OFFICIAL_MODE" = "true" ]; then + CONFIG_USE_CN="false" + fi + + # Set language based on CONFIG_USE_CN if not explicitly set + if [ "$CONFIG_LANG_SET" = "false" ]; then + if [ "$CONFIG_USE_CN" = "true" ]; then + CONFIG_LANG="zh" + else + CONFIG_LANG="en" + fi + fi +} + +# ============================================================================ +# System Detection +# ============================================================================ + +detect_os() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "linux" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macos" + else + echo "unknown" + fi +} + +detect_linux_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + echo "$ID" + elif [ -f /etc/redhat-release ]; then + echo "rhel" + else + echo "unknown" + fi +} + +# ============================================================================ +# Dependency Installation +# ============================================================================ + +install_dependencies_linux() { + local distro + local sudo_cmd="" + + if [ "$EUID" -ne 0 ]; then + sudo_cmd="sudo" + fi + + distro=$(detect_linux_distro) + + case "$distro" in + ubuntu|debian) + log_info "installing_ubuntu" + $sudo_cmd apt-get update -qq + $sudo_cmd apt-get install -y python3 python3-venv python3-pip git gcc libncurses-dev + ;; + suse|opensuse*) + log_info "installing_suse" + $sudo_cmd zypper install -y python3 python3-venv python3-pip git gcc ncurses-devel + ;; + arch|manjaro) + log_info "installing_arch" + $sudo_cmd pacman -S --noconfirm python python-pip git gcc ncurses + ;; + rhel|centos|fedora) + log_info "installing_fedora" + $sudo_cmd dnf install -y python3 python3-venv python3-pip git gcc ncurses-devel + ;; + alpine) + log_info "installing_alpine" + $sudo_cmd apk add --no-cache python3 py3-venv py3-pip git gcc ncurses-dev linux-headers musl-dev + ;; + *) + log_error "unsupported_os" "$distro" + log_info "missing_gcc" + exit 1 + ;; + esac +} + +install_dependencies_macos() { + log_info "installing_macos" + + # Install Homebrew if not installed + if ! command -v brew &> /dev/null; then + log_info "installing_homebrew" + /bin/bash -c "$(curl -fsSL "$HOMEBREW_INSTALL_URL")" + fi + + # Update Homebrew + brew update + + # Install dependencies + brew list python &> /dev/null || brew install python + brew list git &> /dev/null || brew install git + brew list ncurses &> /dev/null || brew install ncurses +} + +# ============================================================================ +# Python Environment Setup +# ============================================================================ + +check_python() { + # Check python3 + if ! command -v python3 &> /dev/null; then + log_error "missing_python" + return 1 + fi + + # Show Python version first + local version + version=$(python3 --version 2>&1) + version=$(echo "$version" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+') + log_info "python_version" "$version" + + # Check python3-venv + if ! python3 -c "import venv" 2>/dev/null; then + return 1 + fi + + # Check pip + if ! python3 -m pip --version &>/dev/null; then + return 1 + fi + + return 0 +} + +# ============================================================================ +# Banner and Next Steps +# ============================================================================ + +print_banner() { + echo "" + echo "============================================================" + echo " $(get_message 'banner_title') " + echo "============================================================" + echo "" +} + +# ============================================================================ +# Main Function +# ============================================================================ + +main() { + set -e # Exit on error + + # Initialize environment + init_environment + + # Parse command line arguments + parse_args "$@" + + # Print help if requested + if [ "$CONFIG_HELP_MODE" = "true" ]; then + print_help + exit 0 + fi + + # Print installation banner + print_banner + + # Log mirror selection result + if [ "$CONFIG_USE_CN" = "true" ]; then + log_info "using_cn_mirror" + else + log_info "using_official_source" + fi + + # Check dependencies (git, python), install if missing + if ! check_git || ! check_python; then + install_dependencies_linux + fi + + # Download and execute touch_env.py + download_and_run_touch_env +} + +# ============================================================================ +# Run Main Function +# ============================================================================ + +main "$@" diff --git a/tools/touch_env.py b/tools/touch_env.py new file mode 100644 index 00000000..3940b88e --- /dev/null +++ b/tools/touch_env.py @@ -0,0 +1,1901 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# File : touch_env.py +# This file is part of RT-Thread RTOS +# COPYRIGHT (C) 2006 - 2026, RT-Thread Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Change Logs: +# Date Author Notes +# 2026-01-30 dongly Initial version +# +# RT-Thread ENV Setup Script (Python) +# RT-Thread ENV 安装脚本 (Python) +# +# This script handles the setup of RT-Thread ENV after the repository is cloned. +# 此脚本在仓库克隆后处理 RT-Thread ENV 的设置。 +# It performs the installation process: +# 执行安装过程: +# 1. Setup repositories (clone packages, sdk, env) - 设置仓库(克隆 packages, sdk, env) +# 2. Create Python virtual environment - 创建 Python 虚拟环境 +# 3. Install Python packages - 安装 Python 包 +# 4. Restore preserved configuration - 恢复保留的配置 +# 5. Show next steps - 显示后续步骤 +# +# Usage: +# 用法: +# python touch_env.py [OPTIONS] +# +# Options: +# 选项: +# --env-root Installation root directory (default: ~/.rt-env) +# 安装 ENV_ROOT(默认:~/.rt-env) +# --use-cn Use China mirror (Gitee, TUNA PyPI) +# 使用中国镜像(Gitee, TUNA PyPI) +# --language Language: 'en' or 'zh' +# 语言:'en' 或 'zh' +# --auto-mode Auto-install without prompts +# 自动安装,无提示 +# --backup Backup strategy when ENV exists: +# 当 ENV 已存在时的备份策略: +# preserve: Keep .config and toolchains(local_pkgs), delete others +# 保留 .config 和 local_pkgs,删除其他内容 +# delete_all: Backup then delete everything, no restore +# 备份后删除所有内容,不恢复 +# delete_all_now: Delete everything immediately, no backup +# 立即删除所有内容,不备份 +# backup_all: Keep backup with hardlink restore +# 保留备份,用硬链接恢复工具链(local_pkgs) +# --install-pyocd Install pyocd for debugging +# 安装 pyocd 调试工具 +# --restore-config Restore preserved configuration +# 恢复保留的配置 +# --repo-env Custom env repository URL, e.g.: +# 自定义 env 仓库 URL,例如: +# https://github.com/user/env.git#branch1 <--- branch is optional +# https://github.com/user/env.git <--- 分支是可选的 +# --repo-packages Custom packages repository URL +# 自定义 packages 仓库 URL +# --repo-sdk Custom sdk repository URL +# 自定义 sdk 仓库 URL +# +# Examples: +# 示例: +# python touch_env.py +# python touch_env.py --backup preserve +# python touch_env.py --env-root /path/to/env +# python touch_env.py --repo-env https://github.com/user/env.git#branch1 +# python touch_env.py --backup delete_all --repo-packages https://github.com/user/packages.git#my-branch +# + +import os +import sys +import argparse +import platform +import shutil +import subprocess +import json +from pathlib import Path +from datetime import datetime + +# ============================================================================ +# Configuration Constants +# ============================================================================ + +# GitHub official sources +REPO_PACKAGES_GITHUB = "https://github.com/RT-Thread/packages.git" +REPO_ENV_GITHUB = "https://github.com/RT-Thread/env.git" +REPO_SDK_GITHUB = "https://github.com/RT-Thread/sdk.git" + +# Gitee mirrors (China) +REPO_PACKAGES_GITEE = "https://gitee.com/RT-Thread-Mirror/packages.git" +REPO_ENV_GITEE = "https://gitee.com/RT-Thread-Mirror/env.git" +REPO_SDK_GITEE = "https://gitee.com/RT-Thread-Mirror/sdk.git" + +# PyPI mirror +PYPI_MIRROR_CN = "https://pypi.tuna.tsinghua.edu.cn/simple" + +# Internal default values +VENV_DIR_RELATIVE = "venv/rt-env" +SCRIPTS_DIR_RELATIVE = "tools/scripts" +TEMP_CONFIG_FILE = ".config.backup" + +# Default installation root directory +DEFAULT_ENV_ROOT = "~/.rt-env" + +# Portable Python directory name +PORTABLE_PYTHON_DIR = "python" + +# Backup configuration constants +BACKUP_MIN_SPACE_GB = 1 # Minimum space required if size calculation fails (GB) +BACKUP_SAFETY_MARGIN = 1.2 # Safety margin for backup space (20%) + +# Platform-specific imports +if platform.system() == 'Windows': + try: + import msvcrt + except ImportError: + msvcrt = None +else: + msvcrt = None + try: + import tty + import termios + HAS_TTY = True + except ImportError: + HAS_TTY = False + +# ============================================================================ +# Python Version Check +# ============================================================================ + +MIN_PYTHON_VERSION = (3, 6) + +if sys.version_info < MIN_PYTHON_VERSION: + print( + f"Error: Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} or higher is required.", file=sys.stderr) + print(f"Current Python version: {sys.version}", file=sys.stderr) + sys.exit(1) + +# ============================================================================ +# Runtime Configuration +# ============================================================================ + +class RuntimeConfig: + """Runtime configuration management""" + def __init__(self): + self._language = 'en' # Default language + + @property + def language(self): + """Get current language""" + return self._language + + @language.setter + def language(self, value): + """Set language""" + self._language = value + +# Global runtime configuration instance +_runtime_config = RuntimeConfig() + +def get_language(): + """Get current language""" + return _runtime_config.language + +def set_language(lang): + """Set language""" + _runtime_config.language = lang + +# ============================================================================ +# TouchEnvConfig Class +# ============================================================================ + + +class TouchEnvConfig: + """Configuration management class for touch_env""" + + def __init__(self, args): + # Check if using default env-root + default_env_root = os.path.expanduser(DEFAULT_ENV_ROOT) + if args.env_root == default_env_root: + # Temporarily set language for log_info + set_language(args.language) + log_info('using_default_env_root', default_env_root) + + # Set language in runtime config + set_language(args.language) + + self.env_root = args.env_root + self.use_cn = args.use_cn + self.language = args.language + self.auto_mode = args.auto_mode + self.install_pyocd = args.install_pyocd + self.restore_config = args.restore_config + self.custom_repos = args.custom_repos + self.backup_strategy = args.backup + + # Backup-related attributes + self.backup_path = None + self.strategy = None # 'preserve' or 'delete_all' or 'backup_all' or None + + # Compute internal paths + self._compute_paths() + + # Validate configuration + self._validate() + + def _compute_paths(self): + """Compute internal paths based on env_root""" + self.venv_dir = os.path.join(self.env_root, VENV_DIR_RELATIVE) + self.scripts_dir = os.path.join(self.env_root, SCRIPTS_DIR_RELATIVE) + self.temp_config_path = os.path.join(self.env_root, TEMP_CONFIG_FILE) + + def _validate(self): + """Validate configuration""" + # Validate env_root + if not self.env_root: + raise ValueError("env_root is required") + + # Validate language + if self.language not in ['en', 'zh']: + raise ValueError( + f"Invalid language: {self.language}. Must be 'en' or 'zh'") + + # custom_repos is built internally in parse_arguments, no need for extensive validation + # Basic type check is sufficient + if self.custom_repos and not isinstance(self.custom_repos, dict): + raise ValueError("custom_repos must be a dictionary") + +# ============================================================================ +# Internationalization Messages +# ============================================================================ + + +MESSAGES = { + 'en': { + 'info': 'INFO', + 'success': 'SUCCESS', + 'warning': 'WARNING', + 'error': 'ERROR', + 'cloning': 'Cloning {0} to {1}', + 'cloned': 'Cloned {0}', + 'dir_exists': 'Directory already exists: {0}', + 'generating_kconfig': 'Generating Kconfig: {0}', + 'creating_venv': 'Creating virtual environment at: {0}', + 'venv_created': 'Virtual environment created', + 'venv_exists': 'Virtual environment already exists', + 'upgrading_pip': 'Upgrading pip...', + 'installing_packages': 'Installing Python packages...', + 'installed_packages': 'Python packages installed successfully', + 'using_cn_mirror': 'Using China mirror', + 'using_pypi_mirror': 'Using PyPI mirror: {0}', + 'copied_env_script': 'Copied env script: {0}', + 'restoring_config': 'Restoring config...', + 'config_restored': 'Config restored', + 'pyocd_install_prompt': 'Do you want to install pyocd (for debugging Cortex-M devices)?', + 'pyocd_install_confirm': 'Install pyocd? [y/N]: ', + 'installing_pyocd': 'Will install pyocd', + 'skipping_pyocd': 'Skipping pyocd installation', + 'install_pyocd_method': '5. To install pyocd, run after activation: `pip install pyocd`', + 'fixed_guiconfig': 'Fixed guiconfig.py (added missing import)', + 'setup_complete': 'RT-Thread ENV installation completed!', + 'next_steps': 'Next steps:', + 'activate_env': '1. Activate environment:', + 'add_to_profile': '2. Add to profile:', + 'install_toolchain': '3. Install toolchains:', + 'install_toolchain_cmd': ' Run `sdk` command to install required toolchains', + 'after_activation': '4. After activation, you can use:', + 'menuconfig': ' - menuconfig : Configure project', + 'menuconfig_s': ' - menuconfig -s : Configure RT-Thread ENV', + 'pkgs': ' - pkgs : Package manager', + 'scons': ' - scons : Build project', + 'sdk': ' - sdk : Install toolchains', + 'clone_failed': 'Git clone failed: {0}', + 'invalid_git_repo': 'Invalid git repository: {0}', + 'venv_not_found': 'Virtual environment not found', + 'package_install_failed': 'Package installation failed: {0}', + 'venv_creation_failed': 'Virtual environment creation failed: {0}', + 'fix_guiconfig_failed': 'Failed to fix guiconfig.py: {0}', + 'using_custom_repo': 'Using custom repository: {0}', + 'using_custom_repo_branch': 'Using custom repository: {0} (branch: {1})', + 'backup_config': 'Backing up configuration file...', + 'no_config_to_restore': 'No configuration to restore', + 'env_root_exists': 'Existing RT-Thread ENV detected at: {0}', + 'env_root_exists_prompt': 'Please select how to handle existing directory (ENV_ROOT)', + 'env_root_confirm': 'Are you sure you want to delete? [Y/A/b/D/C/n]: ', + 'env_root_confirm_help': ' Y/y: Preserve config and toolchains(local_pkgs), delete others (default)', + 'env_root_confirm_all': ' A/a: Backup then delete entire directory (including config and toolchains)', + 'env_root_confirm_backup': ' B/b: Backup entire directory and keep', + 'env_root_confirm_delete': ' D/d: Delete entire directory immediately (including config and toolchains)', + 'env_root_confirm_new': ' C/c: Specify new installation directory', + 'env_root_confirm_no': ' N/n: Cancel installation', + 'use_arrow_keys': 'Use ↑/↓ arrows to select, Enter to confirm', + 'press_enter_confirm': 'Or press Y/A/B/D/C/N directly', + 'installation_cancelled': 'Installation cancelled', + 'installation_failed': 'Installation failed: {0}', + 'skipping_item': 'Skipping: {0}', + 'item_deleted': 'Deleted: {0}', + 'file_delete_failed': 'Failed to delete file: {0} - {1}', + 'dir_delete_failed': 'Failed to delete directory: {0} - {1}', + 'deleting_env_root': 'Deleting existing directory: {0}', + 'deleting_env_root_failed': 'Failed to delete directory: {0}', + 'file_copy_failed': 'Failed to copy file: {0} - {1}', + 'dir_hardlink_failed': 'Failed to hardlink directory: {0} - {1}', + 'restoring_local_pkgs_with_hardlink': 'Restoring local_pkgs with hardlinks...', + 'local_pkgs_restored': 'Toolchains restored with hardlinks', + 'backup_creating': 'Creating backup: {0}...', + 'backup_created': 'Backup created: {0}', + 'backup_restore_failed': 'Failed to restore from backup: {0}', + 'backup_kept_for_manual_recovery': 'Backup kept for manual recovery at: {0}', + 'backup_create_failed': 'Failed to create backup: {0}', + 'manual_backup_required': 'Please manually backup and delete directory, then retry', + 'manual_delete_required': 'Please manually delete directory: {0}, then retry', + 'install_failed_options': 'Installation failed. What would you like to do?', + 'option_restore_backup': ' R/r: Restore from backup (roll back to previous state)', + 'option_keep_current': ' K/k: Keep current state (partial installation)', + 'option_delete_backup': ' D/d: Delete backup and exit', + 'install_failed_prompt': 'Your choice [R/k/d]: ', + 'restore_from_backup': 'Restoring from backup: {0}...', + 'backup_restored': 'Backup restored successfully', + 'backup_cleaned': 'Backup cleaned up: {0}', + 'no_space_for_backup': 'Insufficient disk space for backup. Required: {0}, Available: {1}', + 'checking_disk_space': 'Checking disk space...', + 'auto_restoring_backup': 'Automatically restoring backup...', + 'keeping_current_state': 'Keeping current state as is...', + 'start': '[PY]Starting RT-Thread ENV installation...', + 'using_default_env_root': 'Using default ENV_ROOT: {0}', + 'env_root_prompt': 'Enter installation root directory (ENV_ROOT)', + 'env_root_default': '[default: {0}]', + 'python_path_invalid': 'Path contains {0} (not allowed in Python paths)', + 'python_path_creating_dir': 'Creating directory: {0}', + 'python_path_no_permission': 'No write permission for directory: {0}', + }, + 'zh': { + 'info': '信息', + 'success': '成功', + 'warning': '警告', + 'error': '错误', + 'cloning': '正在克隆: {0} 到 {1}', + 'cloned': '已克隆: {0}', + 'dir_exists': '目录已存在: {0}', + 'generating_kconfig': '生成 Kconfig: {0}', + 'creating_venv': '正在创建虚拟环境: {0}', + 'venv_created': '虚拟环境创建完成', + 'venv_exists': '虚拟环境已存在', + 'upgrading_pip': '正在升级 pip...', + 'installing_packages': '正在安装 Python 包...', + 'installed_packages': 'Python 包安装完成', + 'using_cn_mirror': '使用中国镜像源', + 'using_pypi_mirror': '使用 PyPI 镜像: {0}', + 'copied_env_script': '已复制 env 脚本: {0}', + 'restoring_config': '正在恢复配置...', + 'config_restored': '配置已恢复', + 'pyocd_install_prompt': '是否要安装 pyocd (用于调试 Cortex-M 设备)?', + 'pyocd_install_confirm': '安装 pyocd?[y/N]: ', + 'installing_pyocd': '将要安装 pyocd', + 'skipping_pyocd': '跳过 pyocd 安装', + 'install_pyocd_method': '5. 如需安装 pyocd,激活后运行: `pip install pyocd`', + 'fixed_guiconfig': '已修复 guiconfig.py(添加缺失的导入)', + 'setup_complete': 'RT-Thread ENV 安装完成!', + 'next_steps': '后续步骤:', + 'activate_env': '1. 激活环境:', + 'add_to_profile': '2. 添加到配置文件:', + 'install_toolchain': '3. 安装工具链:', + 'install_toolchain_cmd': ' 运行 `sdk` 命令安装所需的工具链', + 'after_activation': '4. 激活后可用命令:', + 'menuconfig': ' - menuconfig : 配置项目', + 'menuconfig_s': ' - menuconfig -s : 配置 RT-Thread ENV', + 'pkgs': ' - pkgs : 包管理器', + 'scons': ' - scons : 编译项目', + 'sdk': ' - sdk : 安装工具链', + 'clone_failed': 'Git 克隆失败: {0}', + 'invalid_git_repo': '无效的 git 仓库: {0}', + 'venv_not_found': '找不到虚拟环境', + 'package_install_failed': '包安装失败: {0}', + 'venv_creation_failed': '虚拟环境创建失败: {0}', + 'fix_guiconfig_failed': '修复 guiconfig.py 失败: {0}', + 'using_custom_repo': '使用自定义仓库: {0}', + 'using_custom_repo_branch': '使用自定义仓库: {0} (分支: {1})', + 'backup_config': '正在备份配置文件...', + 'no_config_to_restore': '没有需要恢复的配置', + 'env_root_exists': '检测到已存在的 RT-Thread ENV: {0}', + 'env_root_exists_prompt': '检测到已存在的RT-Thread ENV。是否要删除并重新安装?', + 'env_root_confirm': '确定要删除吗?[Y/A/b/D/C/n]: ', + 'env_root_confirm_help': ' Y/y: 保留配置和工具链(local_pkgs),删除其他(默认)', + 'env_root_confirm_all': ' A/a: 备份后删除整个目录(包括配置和工具链)', + 'env_root_confirm_backup': ' B/b: 备份整个目录并保留', + 'env_root_confirm_delete': ' D/d: 立即删除整个目录(包括配置和工具链)', + 'env_root_confirm_new': ' C/c: 指定新的安装目录', + 'env_root_confirm_no': ' N/n: 取消安装', + 'use_arrow_keys': '使用 ↑/↓ 方向键选择,回车确认', + 'press_enter_confirm': '或直接按 Y/A/B/D/C/N 键', + 'installation_cancelled': '安装已取消', + 'installation_failed': '安装失败: {0}', + 'skipping_item': '跳过: {0}', + 'item_deleted': '已删除: {0}', + 'file_delete_failed': '删除文件失败: {0} - {1}', + 'dir_delete_failed': '删除目录失败: {0} - {1}', + 'deleting_env_root': '正在删除现有目录: {0}', + 'deleting_env_root_failed': '删除目录失败: {0}', + 'file_copy_failed': '复制文件失败: {0} - {1}', + 'dir_hardlink_failed': '硬链接目录失败: {0} - {1}', + 'restoring_local_pkgs_with_hardlink': '正在使用硬链接恢复工具链...', + 'local_pkgs_restored': '工具链已使用硬链接恢复', + 'backup_creating': '正在创建备份: {0}...', + 'backup_created': '备份已创建: {0}', + 'backup_restore_failed': '从备份恢复失败: {0}', + 'backup_kept_for_manual_recovery': '备份已保留,可供手动恢复,位置: {0}', + 'backup_create_failed': '创建备份失败: {0}', + 'manual_backup_required': '请手动备份并删除目录后重试', + 'manual_delete_required': '请手动删除目录: {0},然后重试', + 'install_failed_options': '安装失败。您想要怎么做?', + 'option_restore_backup': ' R/r: 从备份恢复(回滚到之前的状态)', + 'option_keep_current': ' K/k: 保留当前状态(部分安装)', + 'option_delete_backup': ' D/d: 删除备份并退出', + 'install_failed_prompt': '您的选择 [R/k/d]: ', + 'restore_from_backup': '正在从备份恢复: {0}...', + 'backup_restored': '备份恢复成功', + 'backup_cleaned': '备份已清理: {0}', + 'no_space_for_backup': '磁盘空间不足以创建备份。需要: {0}, 可用: {1}', + 'checking_disk_space': '正在检查磁盘空间...', + 'auto_restoring_backup': '自动恢复备份中...', + 'keeping_current_state': '保持当前状态不变...', + 'start': '[PY]开始 RT-Thread ENV 安装...', + 'using_default_env_root': '使用默认 ENV_ROOT: {0}', + 'env_root_prompt': '请选择怎样处理现存目录(ENV_ROOT)', + 'env_root_default': '[默认: {0}]', + 'python_path_invalid': '路径包含 {0}(Python 路径中不允许)', + 'python_path_creating_dir': '正在创建目录: {0}', + 'python_path_no_permission': '没有目录的写入权限: {0}', + } + } + +# ============================================================================ +# Global Variables +# ============================================================================ + +# ============================================================================ +# Message Functions +# ============================================================================ + + +def get_message(key): + """Get localized message using current language""" + lang = get_language() + return MESSAGES.get(lang, {}).get(key, key) + + +def log_info(key, *args): + """Log info message to stdout""" + msg = get_message(key) + if args: + msg = msg.format(*args) + print(f"\033[0;36m[{get_message('info')}]\033[0m {msg}") + + +def log_success(key, *args): + """Log success message to stdout""" + msg = get_message(key) + if args: + msg = msg.format(*args) + print(f"\033[0;32m[{get_message('success')}]\033[0m {msg}") + + +def log_error(key, *args): + """Log error message to stderr""" + msg = get_message(key) + if args: + msg = msg.format(*args) + print(f"\033[0;31m[{get_message('error')}]\033[0m {msg}", file=sys.stderr) + + +def log_warning(key, *args): + """Log warning message to stderr""" + msg = get_message(key) + if args: + msg = msg.format(*args) + print(f"\033[0;33m[{get_message('warning')}]\033[0m {msg}", file=sys.stderr) + + +def log_raw(key, *args, **kwargs): + """Log raw message using current language""" + msg = get_message(key) + if args: + msg = msg.format(*args) + print(msg, **kwargs) + +# ============================================================================ +# Repository Functions +# ============================================================================ + + +def clone_repository(config, repo_name, url, dest_rel, branch='', depth=1): + """ + Clone Git repository with cleanup on failure + + Args: + config: TouchEnvConfig instance + repo_name: Repository name ('packages', 'sdk', or 'env') + url: Repository URL + dest_rel: Destination path relative to env_root + branch: Optional branch name + depth: Clone depth (default 1 for shallow clone) + + Raises: + RuntimeError: If clone fails + """ + dest_path = os.path.join(config.env_root, dest_rel) + + # If directory exists, verify it's a valid git repository + if os.path.exists(dest_path): + try: + result = subprocess.run( + ['git', 'rev-parse', '--git-dir'], + cwd=dest_path, + capture_output=True, + text=True, + check=True + ) + log_success('dir_exists', dest_path) + return + except subprocess.CalledProcessError: + # Invalid git repository, need to clean up + log_error('invalid_git_repo', dest_path) + shutil.rmtree(dest_path, ignore_errors=True) + + # Clone repository + log_info('cloning', url, dest_path) + + clone_args = ['git', 'clone', '--depth', str(depth)] + if branch: + clone_args.extend(['--branch', branch]) + clone_args.extend([url, dest_path]) + + try: + # Run without capture to show verbose git output + subprocess.run(clone_args, check=True) + log_success('cloned', dest_path) + except subprocess.CalledProcessError as e: + # Clone failed, clean up partial clone + log_error('clone_failed', str(e)) + shutil.rmtree(dest_path, ignore_errors=True) + raise RuntimeError(f"Failed to clone {url}") from e + + +def setup_repositories(config): + """ + Setup all repositories (packages, sdk, env) + + Args: + config: TouchEnvConfig instance + + Raises: + RuntimeError: If any repository setup fails + """ + # Base repositories + github_repos = { + 'packages': REPO_PACKAGES_GITHUB, + 'env': REPO_ENV_GITHUB, + 'sdk': REPO_SDK_GITHUB + } + + gitee_repos = { + 'packages': REPO_PACKAGES_GITEE, + 'env': REPO_ENV_GITEE, + 'sdk': REPO_SDK_GITEE + } + + # Select mirror + repos_base = gitee_repos if config.use_cn else github_repos + + # Repository destinations + repo_dests = { + 'packages': 'packages/packages', + 'env': 'tools/scripts', + 'sdk': 'packages/sdk' + } + + # Clone all repositories + for repo_name in ['packages', 'env', 'sdk']: + # Check for custom repository + if config.custom_repos and repo_name in config.custom_repos: + repo_info = config.custom_repos[repo_name] + url = repo_info['url'] + branch = repo_info.get('branch', '') + + if branch: + log_info('using_custom_repo_branch', url, branch) + else: + log_info('using_custom_repo', url) + else: + url = repos_base[repo_name] + branch = '' + + clone_repository(config, repo_name, url, repo_dests[repo_name], branch) + + # Generate Kconfig file + generate_kconfig_file(config) + + # Copy env scripts + copy_env_scripts(config) + + +def generate_kconfig_file(config): + """Generate Kconfig configuration file""" + packages_dir = os.path.join(config.env_root, 'packages') + os.makedirs(packages_dir, exist_ok=True) + + kconfig_path = os.path.join(packages_dir, 'Kconfig') + kconfig_content = 'source "$PKGS_DIR/packages/Kconfig"\n' + + with open(kconfig_path, 'w', encoding='utf-8') as f: + f.write(kconfig_content) + + log_success('generating_kconfig', kconfig_path) + + # Create local_pkgs directory + local_pkgs_dir = os.path.join(config.env_root, 'local_pkgs') + os.makedirs(local_pkgs_dir, exist_ok=True) + + +def copy_env_scripts(config): + """Copy env scripts to root directory""" + scripts_dir = os.path.join(config.env_root, 'tools/scripts') + + # Copy appropriate script based on platform + if platform.system() == 'Windows': + src = os.path.join(scripts_dir, 'env.ps1') + dst = os.path.join(config.env_root, 'env.ps1') + else: + src = os.path.join(scripts_dir, 'env.sh') + dst = os.path.join(config.env_root, 'env.sh') + + if os.path.exists(src): + shutil.copy2(src, dst) + log_success('copied_env_script', dst) + +# ============================================================================ +# Virtual Environment Functions +# ============================================================================ + + +def create_venv(config): + """ + Create Python virtual environment + + Args: + config: TouchEnvConfig instance + + Raises: + RuntimeError: If venv creation fails + """ + venv_path = config.venv_dir + + if os.path.exists(venv_path): + log_success('venv_exists') + return + + log_info('creating_venv', venv_path) + + try: + import venv + venv.create(venv_path, with_pip=True) + log_success('venv_created') + except (OSError, PermissionError, ValueError) as e: + log_error('venv_creation_failed', str(e)) + raise RuntimeError(f"Failed to create virtual environment: {e}") from e + + +def get_python_executable(config): + """ + Get virtual environment Python executable path + + Args: + config: TouchEnvConfig instance + + Returns: + Path to Python executable + """ + if platform.system() == 'Windows': + return os.path.join(config.venv_dir, 'Scripts', 'python.exe') + else: + return os.path.join(config.venv_dir, 'bin', 'python') + +# ============================================================================ +# Package Installation Functions +# ============================================================================ + + +def install_packages(config): + """ + Install Python packages + + Args: + config: TouchEnvConfig instance + + Raises: + RuntimeError: If package installation fails + """ + python_exe = get_python_executable(config) + + if not os.path.exists(python_exe): + log_error('venv_not_found') + raise RuntimeError("Virtual environment not found") + + # Upgrade pip + log_info('upgrading_pip') + subprocess.run( + [python_exe, '-m', 'pip', 'install', '--upgrade', 'pip'], + check=True + ) + + # Build pip install arguments + pip_args = [python_exe, '-m', 'pip', 'install'] + + # Add mirror source + if config.use_cn: + log_info('using_cn_mirror') + log_info('using_pypi_mirror', PYPI_MIRROR_CN) + pip_args.extend(['--index-url', PYPI_MIRROR_CN]) + + # Install rt-env package (editable mode) + pip_args.extend(['-e', config.scripts_dir]) + + # Optionally install pyocd + if config.install_pyocd: + pip_args.append('pyocd') + + # Execute installation + log_info('installing_packages') + try: + subprocess.run(pip_args, check=True) + log_success('installed_packages') + except subprocess.CalledProcessError as e: + log_error('package_install_failed', str(e)) + raise RuntimeError(f"Package installation failed: {e}") from e + + # Fix guiconfig.py missing import re issue + fix_guiconfig_import(config) + + +def fix_guiconfig_import(config): + """ + Fix guiconfig.py missing import re issue + + Args: + config: TouchEnvConfig instance + """ + # Direct path for Windows and Unix-like systems + if platform.system() == 'Windows': + guiconfig_path = os.path.join( + config.venv_dir, 'Lib', 'site-packages', 'guiconfig.py') + else: + guiconfig_path = os.path.join( + config.venv_dir, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages', 'guiconfig.py') + + if not os.path.exists(guiconfig_path): + return + + try: + with open(guiconfig_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Check if import re already exists + if 'import re' not in content: + # Insert import re at the appropriate location + lines = content.split('\n') + + # Find the first non-comment, non-docstring line + # Skip shebang, encoding, and docstring + import_index = 0 + in_docstring = False + docstring_delimiter = None + + for i, line in enumerate(lines): + stripped = line.strip() + + # Skip empty lines and comments + if not stripped or stripped.startswith('#'): + continue + + # Handle docstring + if (stripped.startswith('"""') or stripped.startswith("'''")): + if in_docstring: + if stripped.startswith(docstring_delimiter) and len(stripped) > 3: + in_docstring = False + else: + in_docstring = True + docstring_delimiter = stripped[:3] + continue + + if in_docstring: + continue + + # Found first actual code line + # Look for the first import or from statement + if line.startswith('import ') or line.startswith('from '): + import_index = i + 1 + else: + import_index = i + break + + # Insert import re at the calculated position + lines.insert(import_index, 'import re') + content = '\n'.join(lines) + + with open(guiconfig_path, 'w', encoding='utf-8') as f: + f.write(content) + + log_success('fixed_guiconfig') + except (IOError, PermissionError, UnicodeDecodeError, UnicodeEncodeError) as e: + # Fix failure should not interrupt installation + log_error('fix_guiconfig_failed', str(e)) + +# ============================================================================ +# Configuration Backup/Restore Functions +# ============================================================================ + + +def backup_config_file(config): + """ + Backup configuration file + + Args: + config: TouchEnvConfig instance + """ + config_path = os.path.join( + config.env_root, 'tools', 'scripts', 'cmds', '.config') + + if os.path.exists(config_path): + log_info('backup_config') + shutil.copy2(config_path, config.temp_config_path) + + +def restore_config(config): + """ + Restore configuration file + + Args: + config: TouchEnvConfig instance + """ + if config.restore_config and os.path.exists(config.temp_config_path): + config_path = os.path.join( + config.env_root, 'tools', 'scripts', 'cmds', '.config') + + log_info('restoring_config') + shutil.copy2(config.temp_config_path, config_path) + os.remove(config.temp_config_path) + log_success('config_restored') + else: + log_info('no_config_to_restore') + +# ============================================================================ +# Check Existing ENV Functions +# ============================================================================ + + +def check_existing_env(config): + """ + Check if existing ENV exists and handle backup + + Args: + config: TouchEnvConfig instance + + Raises: + SystemExit: If user cancels installation + """ + if not os.path.exists(config.env_root): + return + + log_raw(get_message('env_root_exists').format(config.env_root)) + print() + + # Check if backup strategy is specified via command line + if config.backup_strategy: + # Use specified strategy directly, skip interactive menu + config.strategy = config.backup_strategy + try: + config.backup_path = create_backup_directory(config) + except (OSError, RuntimeError) as e: + log_error('backup_create_failed', str(e)) + log_warning('manual_backup_required') + sys.exit(1) + elif config.auto_mode: + # Auto mode: use specified strategy or default to 'preserve' + if config.backup_strategy: + config.strategy = config.backup_strategy + else: + config.strategy = 'preserve' + try: + config.backup_path = create_backup_directory(config) + except (OSError, RuntimeError) as e: + log_error('backup_create_failed', str(e)) + sys.exit(1) + else: + # Interactive mode: ask user + response = show_deletion_options(config) + + if response.lower() == 'y': + # Preserve config and local_pkgs + config.strategy = 'preserve' + try: + config.backup_path = create_backup_directory(config) + except (OSError, RuntimeError) as e: + log_error('backup_create_failed', str(e)) + log_warning('manual_backup_required') + sys.exit(1) + elif response.lower() == 'a': + # Backup then delete everything + config.strategy = 'delete_all' + try: + config.backup_path = create_backup_directory(config) + except (OSError, RuntimeError) as e: + log_error('backup_create_failed', str(e)) + log_warning('manual_backup_required') + sys.exit(1) + elif response.lower() == 'b': + # Backup entire directory, then delete everything + config.strategy = 'backup_all' + try: + config.backup_path = create_backup_directory(config) + except (OSError, RuntimeError) as e: + log_error('backup_create_failed', str(e)) + log_warning('manual_backup_required') + sys.exit(1) + elif response.lower() == 'd': + # Delete everything immediately without backup + config.strategy = 'delete_all_now' + config.backup_path = None + # Immediately delete the directory + if os.path.exists(config.env_root): + log_info('deleting_env_root', config.env_root) + try: + _safe_remove_tree(config.env_root) + except (OSError, PermissionError) as e: + log_error('deleting_env_root_failed', str(e)) + log_warning('manual_delete_required', config.env_root) + sys.exit(1) + elif response.lower() == 'c': + # Specify new installation directory + default_env_root = os.path.expanduser(DEFAULT_ENV_ROOT) + config.env_root = prompt_env_root(default_env_root, config.language) + config._compute_paths() + # Re-check existing ENV with new path + check_existing_env(config) # 递归调用以检查新路径 + return # 退出当前函数 + else: + # Cancel installation + log_info('installation_cancelled') + sys.exit(0) + + +def show_deletion_options(config): + """ + Show deletion options to user with interactive menu selection + + Args: + config: TouchEnvConfig instance + + Returns: + str: User response (y/a/b/n) + """ + # Define options + options = [ + {'key': 'Y', 'desc': get_message('env_root_confirm_help'), 'default': True}, + {'key': 'A', 'desc': get_message('env_root_confirm_all'), 'default': False}, + {'key': 'B', 'desc': get_message('env_root_confirm_backup'), 'default': False}, + {'key': 'D', 'desc': get_message('env_root_confirm_delete'), 'default': False}, + {'key': 'C', 'desc': get_message('env_root_confirm_new'), 'default': False}, + {'key': 'N', 'desc': get_message('env_root_confirm_no'), 'default': False}, + ] + + # Try interactive menu if msvcrt (Windows) or tty (Linux) is available + if msvcrt is not None or HAS_TTY: + try: + return _interactive_menu(options) + except Exception: + # Fall back to simple input if interactive menu fails + pass + + # Fallback to simple input + log_raw('env_root_confirm', end='', flush=True) + print() + for opt in options: + print(opt['desc']) + print('> ', end='', flush=True) + + try: + response = input().strip().lower() + if not response: + return 'y' # Default is y (preserve) + return response + except KeyboardInterrupt: + print() + log_info('installation_cancelled') + sys.exit(0) + + +def _get_key_linux(): + """Get single key press on Linux""" + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + key = sys.stdin.read(1) + if key == '\x1b': # Escape sequence + key += sys.stdin.read(2) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return key + + +def _interactive_menu(options): + """ + Interactive menu with arrow key navigation + + Args: + options: List of option dictionaries with 'key', 'desc', 'default' + + Returns: + str: Selected option key (lowercase) + """ + selected_index = 0 + + # Find default option + for i, opt in enumerate(options): + if opt.get('default', False): + selected_index = i + break + + # Calculate lines to clear (prompt + blank + options + blank + 2 help lines = 5 + len(options)) + lines_to_clear = 5 + len(options) + first_run = True + + while True: + if first_run: + # First run: print empty lines to overwrite previous output + print('\n' * lines_to_clear, end='') + # Move cursor up to the beginning of menu area + print(f'\033[{lines_to_clear}A', end='') + else: + # Clear only the menu area + print(f'\033[{lines_to_clear}M\033[{lines_to_clear}A', end='') + first_run = False + + log_raw('env_root_exists_prompt') + print() + + for i, opt in enumerate(options): + if i == selected_index: + # Highlight selected option + print(f"\033[7m > {opt['desc']}\033[0m") + else: + print(f" {opt['desc']}") + + print() + log_raw('use_arrow_keys') + log_raw('press_enter_confirm') + + # Read key - Windows (msvcrt) + if msvcrt: + key = msvcrt.getch() + if key == b'\xe0': # Special key prefix + key = msvcrt.getch() + if key == b'H': # Up arrow + selected_index = (selected_index - 1) % len(options) + elif key == b'P': # Down arrow + selected_index = (selected_index + 1) % len(options) + elif key == b'\r' or key == b'\n': # Enter key + return options[selected_index]['key'].lower() + elif key == b'\x03': # Ctrl+C + print() + log_info('installation_cancelled') + sys.exit(0) + elif key in [b'y', b'Y', b'a', b'A', b'b', b'B', b'c', b'C', b'n', b'N']: + # Direct key press + return key.decode('ascii').lower() + # Read key - Linux + elif HAS_TTY: + key = _get_key_linux() + if key == '\x1b[A': # Up arrow + selected_index = (selected_index - 1) % len(options) + elif key == '\x1b[B': # Down arrow + selected_index = (selected_index + 1) % len(options) + elif key == '\r' or key == '\n': # Enter key + return options[selected_index]['key'].lower() + elif key == '\x03': # Ctrl+C + print() + log_info('installation_cancelled') + sys.exit(0) + elif key.lower() in ['y', 'a', 'b', 'c', 'n']: + # Direct key press + return key.lower() + + +def get_backup_timestamp(): + """ + Generate timestamp for backup directory naming + + Returns: + str: Timestamp in format YYYYMMDD_HHMMSS + """ + return datetime.now().strftime("%Y%m%d_%H%M%S") + + +def create_backup_directory(config): + """ + Create backup directory with timestamp + + Args: + config: TouchEnvConfig instance with env_root path + + Returns: + str: Path to the backup directory + + Raises: + OSError: If backup creation fails + RuntimeError: If insufficient disk space + """ + log_info('checking_disk_space') + + # Get backup directory size estimate (optimized for large directories) + env_size = 0 + try: + for dirpath, _, filenames in os.walk(config.env_root): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + try: + env_size += os.path.getsize(filepath) + except (OSError, PermissionError): + # Skip files we can't access + continue + except OSError as e: + log_error('backup_create_failed', f'Failed to calculate size: {e}') + # Continue anyway, as size estimate is just a safety check + env_size = 0 + + # Check disk space + disk_usage = shutil.disk_usage(os.path.dirname(config.env_root)) + available_space = disk_usage.free + + # If we couldn't calculate size, require minimum 1GB + if env_size == 0: + required_space = 1024 * 1024 * 1024 # 1GB + else: + # Require 20% extra space as safety margin + required_space = int(env_size * 1.2) + + if available_space < required_space: + log_error('no_space_for_backup', + f'{required_space // (1024*1024)} MB', + f'{available_space // (1024*1024)} MB') + raise RuntimeError(f'Insufficient disk space for backup') + + # Generate backup path + timestamp = get_backup_timestamp() + backup_name = f"{os.path.basename(config.env_root)}.backup.{timestamp}" + backup_path = os.path.join(os.path.dirname(config.env_root), backup_name) + + # Check if backup already exists + if os.path.exists(backup_path): + log_error('backup_create_failed', f'Backup directory already exists: {backup_path}') + raise RuntimeError(f'Backup directory already exists: {backup_path}') + + # Create backup by renaming + log_info('backup_creating', backup_path) + + try: + shutil.move(config.env_root, backup_path) + log_success('backup_created', backup_path) + except (OSError, PermissionError) as e: + log_error('backup_create_failed', str(e)) + raise + + return backup_path + + +def restore_backup(config, backup_path, preserve_items=True): + """ + Restore items from backup directory + + Args: + config: TouchEnvConfig instance with env_root path + backup_path: Path to backup directory + preserve_items: If True, restore preserved items (.config, local_pkgs) + If False, delete backup without restoring + + Returns: + bool: True if operation succeeded, False otherwise + """ + failed_items = [] + + if not preserve_items: + # Strategy A: Delete backup without restoring + log_info('backup_cleaned', backup_path) + _safe_remove_tree(backup_path) + return True + + # Strategy Y: Restore preserved items + # Check if env_root exists + if not os.path.exists(config.env_root): + log_error('backup_restore_failed', f'env_root does not exist: {config.env_root}') + return False + + # 1. Restore .config with hardlink + config_src = os.path.join(backup_path, 'tools', 'scripts', 'cmds', '.config') + config_dst = os.path.join(config.env_root, 'tools', 'scripts', 'cmds', '.config') + + if os.path.exists(config_src): + log_info('restoring_config') + try: + os.makedirs(os.path.dirname(config_dst), exist_ok=True) + # Use hardlink for config file + if os.path.exists(config_dst): + os.remove(config_dst) + os.link(config_src, config_dst) + log_success('config_restored') + except OSError: + # Fallback to copy if hardlink fails (e.g., cross-device) + shutil.copy2(config_src, config_dst) + log_success('config_restored') + else: + log_info('skipping_item', '.config') + + # 2. Restore local_pkgs with hardlinks + local_pkgs_src = os.path.join(backup_path, 'local_pkgs') + local_pkgs_dst = os.path.join(config.env_root, 'local_pkgs') + + if os.path.exists(local_pkgs_src): + log_info('restore_from_backup', 'local_pkgs') + try: + # Remove existing local_pkgs if any + if os.path.exists(local_pkgs_dst): + _safe_remove_tree(local_pkgs_dst) + + os.makedirs(local_pkgs_dst, exist_ok=True) + + # Create hardlinks for all files in local_pkgs + _hardlink_directory(local_pkgs_src, local_pkgs_dst) + log_success('backup_restored', 'local_pkgs') + except (OSError, PermissionError) as e: + log_error('backup_restore_failed', f'local_pkgs: {e}') + failed_items.append('local_pkgs') + else: + log_info('skipping_item', 'local_pkgs') + + # 3. Delete backup directory + log_info('backup_cleaned', backup_path) + _safe_remove_tree(backup_path) + + # Report result + if failed_items: + log_warning('backup_restore_failed', ', '.join(failed_items)) + return False + + return True + + +def cleanup_backup_directory(backup_path): + """ + Safely delete backup directory + + Args: + backup_path: Path to backup directory + """ + if os.path.exists(backup_path): + log_info('backup_cleaned', backup_path) + _safe_remove_tree(backup_path) + + +def restore_with_hardlink(config, backup_path): + """ + Restore config and local_pkgs using hardlink for backup_all strategy + + This function: + - Copies .config file + - Creates hardlinks for local_pkgs + - Keeps backup directory + + Args: + config: TouchEnvConfig instance with env_root path + backup_path: Path to backup directory + + Returns: + bool: True if operation succeeded, False otherwise + """ + if not os.path.exists(config.env_root): + log_error('backup_restore_failed', f'env_root does not exist: {config.env_root}') + return False + + if not os.path.exists(backup_path): + log_warning('no_config_to_restore') + return False + + # 1. Copy .config file + config_src = os.path.join(backup_path, 'tools', 'scripts', 'cmds', '.config') + config_dst = os.path.join(config.env_root, 'tools', 'scripts', 'cmds', '.config') + + if os.path.exists(config_src): + log_info('restoring_config') + try: + os.makedirs(os.path.dirname(config_dst), exist_ok=True) + shutil.copy2(config_src, config_dst) + log_success('config_restored') + except (OSError, PermissionError) as e: + log_error('file_copy_failed', '.config', str(e)) + else: + log_info('skipping_item', '.config') + + # 2. Hardlink local_pkgs + local_pkgs_src = os.path.join(backup_path, 'local_pkgs') + local_pkgs_dst = os.path.join(config.env_root, 'local_pkgs') + + if os.path.exists(local_pkgs_src): + log_info('restoring_local_pkgs_with_hardlink') + try: + # Remove existing local_pkgs if any + if os.path.exists(local_pkgs_dst): + shutil.rmtree(local_pkgs_dst, ignore_errors=True) + + os.makedirs(local_pkgs_dst, exist_ok=True) + + # Create hardlinks for all files in local_pkgs + _hardlink_directory(local_pkgs_src, local_pkgs_dst) + log_success('local_pkgs_restored') + except (OSError, PermissionError) as e: + log_error('dir_hardlink_failed', 'local_pkgs', str(e)) + return False + else: + log_info('skipping_item', 'local_pkgs') + + log_info('backup_kept_for_manual_recovery', backup_path) + return True + + +def _hardlink_directory(src_dir, dst_dir): + """ + Recursively create hardlinks from src_dir to dst_dir + + Args: + src_dir: Source directory path + dst_dir: Destination directory path + + Raises: + OSError: If hardlink creation fails + """ + if not os.path.exists(src_dir): + return + + for item in os.listdir(src_dir): + src_path = os.path.join(src_dir, item) + dst_path = os.path.join(dst_dir, item) + + if os.path.isdir(src_path): + os.makedirs(dst_path, exist_ok=True) + _hardlink_directory(src_path, dst_path) + elif os.path.isfile(src_path): + try: + # Remove destination if it exists + if os.path.exists(dst_path): + os.remove(dst_path) + # Create hardlink + os.link(src_path, dst_path) + except OSError as e: + # Fallback to copy if hardlink fails (e.g., cross-device) + shutil.copy2(src_path, dst_path) + + +def handle_installation_failure(config, backup_path, strategy): + """ + Handle installation failure by offering recovery options + + Args: + config: TouchEnvConfig instance + backup_path: Path to backup directory + strategy: User's original strategy ('preserve' or 'delete_all' or 'backup_all') + + Returns: + int: Exit code (0 for continue, 1 for exit) + """ + # In auto mode, automatically restore backup if it exists + if config.auto_mode: + if backup_path and os.path.exists(backup_path): + log_info('auto_restoring_backup') + restore_backup(config, backup_path, preserve_items=True) + return 1 + + # Interactive mode: ask user what to do + print() + log_raw('install_failed_options') + print() + log_raw('option_restore_backup') + log_raw('option_keep_current') + log_raw('option_delete_backup') + print() + + response = input(get_message('install_failed_prompt')) + + if not response: + response = 'k' # Default: keep current + + response = response.lower() + + if response == 'r': + # Restore from backup + if backup_path and os.path.exists(backup_path): + log_info('restore_from_backup', backup_path) + success = restore_backup(config, backup_path, preserve_items=True) + if success: + log_success('backup_restored') + else: + log_warning('backup_restore_failed') + else: + log_info('no_config_to_restore') + return 1 + elif response == 'd': + # Delete backup only + if backup_path and os.path.exists(backup_path): + cleanup_backup_directory(backup_path) + log_info('installation_cancelled') + return 1 + else: + # Keep current state (default) + log_info('keeping_current_state') + if backup_path and os.path.exists(backup_path): + log_warning('backup_kept_for_manual_recovery', backup_path) + return 1 + + +def _safe_remove(path, name): + """ + Safely remove a file or directory with error handling + + Args: + path: Full path to the file or directory + name: Name of the item (for logging) + + Returns: + bool: True if removal succeeded, False otherwise + """ + try: + if os.path.isfile(path) or os.path.islink(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path) + log_info('item_deleted', name) + return True + except (OSError, PermissionError) as e: + if os.path.isfile(path) or os.path.islink(path): + log_error('file_delete_failed', name, str(e)) + else: + log_error('dir_delete_failed', name, str(e)) + return False + + +def _safe_remove_tree(path): + """ + Safely remove a directory tree, trying multiple methods + + Args: + path: Path to directory to remove + """ + if not os.path.exists(path): + return + + # Method 1: Try rmtree with ignore_errors first + shutil.rmtree(path, ignore_errors=True) + + # Method 2: If still exists, retry with onerror handler + if os.path.exists(path): + def onerror(func, path, exc_info): + # Try to change permissions and retry + try: + os.chmod(path, 0o700) + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + else: + os.remove(path) + except Exception: + pass # Ignore if still fails + + shutil.rmtree(path, onerror=onerror) + + # Method 3: If still exists, list and delete individually + if os.path.exists(path): + for item in os.listdir(path): + item_path = os.path.join(path, item) + try: + if os.path.isfile(item_path) or os.path.islink(item_path): + os.chmod(item_path, 0o700) + os.remove(item_path) + elif os.path.isdir(item_path): + _safe_remove_tree(item_path) + except Exception: + pass # Ignore if fails + + # Finally try to remove the directory itself + try: + os.rmdir(path) + except Exception: + pass # Ignore if fails + + +# ============================================================================ +# User Interaction Functions +# ============================================================================ + + +def prompt_pyocd(config): + """ + Prompt user for pyocd installation + + Args: + config: TouchEnvConfig instance + + Returns: + bool: Whether to install pyocd + """ + # Skip in auto mode + if config.auto_mode: + return False + + # Only prompt on Windows and macOS + if platform.system() not in ['Windows', 'Darwin']: + return False + + print() + log_raw('pyocd_install_prompt') + response = input(get_message('pyocd_install_confirm')) + + install_pyocd = response.lower() == 'y' + if install_pyocd: + log_info('installing_pyocd') + else: + log_info('skipping_pyocd') + + return install_pyocd + + +def show_next_steps(config): + """ + Show installation completion and next steps + + Args: + config: TouchEnvConfig instance + """ + print() + print("=" * 60) + log_success('setup_complete') + print("=" * 60) + print() + log_info('next_steps') + print() + + # Activate environment + log_raw('activate_env') + if platform.system() == 'Windows': + print(f" . {config.env_root}\\env.ps1") + else: + print(f" source {config.env_root}/env.sh") + print() + + # Add to profile + log_raw('add_to_profile') + if platform.system() == 'Windows': + print(f" echo '. {config.env_root}\\env.ps1' >> $PROFILE") + print(f" . $PROFILE") + else: + shell = os.path.basename(os.getenv('SHELL', 'bash')) + profile_file = '~/.zshrc' if 'zsh' in shell else '~/.bashrc' + print(f" echo 'source {config.env_root}/env.sh' >> {profile_file}") + print(f" source {profile_file}") + print() + + # Install toolchain + log_raw('install_toolchain') + print(f" {get_message('install_toolchain_cmd')}") + print() + + # Available commands + log_raw('after_activation') + print(f"{get_message('menuconfig')}") + print(f"{get_message('menuconfig_s')}") + print(f"{get_message('pkgs')}") + print(f"{get_message('scons')}") + print(f"{get_message('sdk')}") + print() + + # Install pyocd if it was skipped + if not config.install_pyocd: + log_raw('install_pyocd_method') + print() + +# ============================================================================ +# Argument Parsing +# ============================================================================ + + +def parse_repo_url(url): + """ + Parse repository URL and extract branch from fragment (#branch) + + Args: + url: Repository URL with optional branch fragment (e.g., https://github.com/user/repo.git#branch1) + + Returns: + dict: {'url': 'https://github.com/user/repo.git', 'branch': 'branch1'} + or {'url': 'https://github.com/user/repo.git'} if no branch specified + """ + from urllib.parse import urlparse, urlunparse + + parsed = urlparse(url) + repo_info = {'url': urlunparse(parsed._replace(fragment=''))} + + if parsed.fragment: + repo_info['branch'] = parsed.fragment + + return repo_info + + +def prompt_env_root(default_env_root, language='en'): + """ + Prompt user to enter env-root directory + + Args: + default_env_root: Default installation directory + language: Language code ('en' or 'zh') + + Returns: + str: User input env-root directory + """ + # Set language for messages + set_language(language) + + env_root = "" + is_valid = False + + while not is_valid: + # Display prompt with default value + prompt_msg = get_message('env_root_prompt') + default_msg = get_message('env_root_default').format(default_env_root) + print(f"{prompt_msg} {default_msg}", end=' ') + env_root = input().strip() + + # Use default if input is empty + if not env_root: + env_root = default_env_root + + # Expand user home directory + env_root = os.path.expanduser(env_root) + + # Check path format (spaces, non-ASCII characters) + if ' ' in env_root: + log_error('python_path_invalid', 'spaces') + continue + if any(ord(c) > 127 for c in env_root): + log_error('python_path_invalid', 'non-ASCII characters') + continue + + # Check if parent directory exists or can be created + parent_dir = os.path.dirname(env_root) + if parent_dir and not os.path.exists(parent_dir): + log_info('python_path_creating_dir', parent_dir) + try: + os.makedirs(parent_dir, exist_ok=True) + except Exception: + log_error('python_path_no_permission', parent_dir) + continue + + # Check write permission + if parent_dir: + test_file = os.path.join(parent_dir, '.__write_test__') + try: + with open(test_file, 'w') as f: + f.write('test') + os.remove(test_file) + except Exception: + log_error('python_path_no_permission', parent_dir) + continue + + is_valid = True + + return env_root + + +def prompt_env_root_if_needed(config, args): + """ + Prompt for env-root if needed (interactive mode, not explicitly specified) + + Args: + config: TouchEnvConfig instance + args: Parsed command line arguments + """ + if not config.auto_mode: + # Check if --env-root was explicitly provided + import sys + has_explicit_env_root = False + for i in range(len(sys.argv)): + if sys.argv[i] == '--env-root' and i + 1 < len(sys.argv): + has_explicit_env_root = True + break + elif sys.argv[i].startswith('--env-root='): + has_explicit_env_root = True + break + + if not has_explicit_env_root: + default_env_root = os.path.expanduser(DEFAULT_ENV_ROOT) + config.env_root = prompt_env_root(default_env_root, config.language) + # Recompute paths with new env_root + config._compute_paths() + + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description='RT-Thread ENV Setup Script', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + '--env-root', + required=False, + default=os.path.expanduser(DEFAULT_ENV_ROOT), + help='Installation root directory (default: ~/.rt-env)' + ) + parser.add_argument( + '--use-cn', + action='store_true', + help='Use China mirror (Gitee, TUNA PyPI)' + ) + parser.add_argument( + '--language', + choices=['en', 'zh'], + default='en', + help='Language (en/zh)' + ) + parser.add_argument( + '--auto-mode', + action='store_true', + help='Auto-install without prompts' + ) + parser.add_argument( + '--install-pyocd', + action='store_true', + help='Install pyocd for debugging' + ) + parser.add_argument( + '--restore-config', + action='store_true', + help='Restore preserved configuration' + ) + parser.add_argument( + '--repo-env', + type=str, + default='', + help='Custom env repository URL' + ) + parser.add_argument( + '--repo-packages', + type=str, + default='', + help='Custom packages repository URL' + ) + parser.add_argument( + '--repo-sdk', + type=str, + default='', + help='Custom sdk repository URL' + ) + parser.add_argument( + '--backup', + choices=['preserve', 'delete_all', 'backup_all'], + help='Backup strategy: preserve (keep config and local_pkgs), delete_all (delete all), backup_all (hardlink restore)' + ) + + args = parser.parse_args() + + # Build custom_repos dictionary from individual arguments + args.custom_repos = {} + if args.repo_env: + args.custom_repos['env'] = parse_repo_url(args.repo_env) + if args.repo_packages: + args.custom_repos['packages'] = parse_repo_url(args.repo_packages) + if args.repo_sdk: + args.custom_repos['sdk'] = parse_repo_url(args.repo_sdk) + + return args + +# ============================================================================ +# Main Execution Function +# ============================================================================ + + +def run_touch_env(args): + """ + Main execution function + + Args: + args: Parsed command line arguments + + Returns: + int: Exit code (0 for success, non-zero for failure) + """ + config = None + + try: + # Step 0: Initialize configuration + config = TouchEnvConfig(args) + + # Step 1: Interactive mode: prompt for env-root if needed + prompt_env_root_if_needed(config, args) + + # Step 2: Check existing ENV and create backup + check_existing_env(config) + + # Step 3: Backup configuration file (from old installation if any) + backup_config_file(config) + + # Step 4: Setup repositories + setup_repositories(config) + + # Step 5: Create virtual environment + create_venv(config) + + # Step 6: Prompt for pyocd installation + if not config.install_pyocd: + config.install_pyocd = prompt_pyocd(config) + + # Step 7: Install packages + install_packages(config) + + # Step 8: Restore configuration + restore_config(config) + + # Step 9: Handle backup based on strategy + if config.backup_path and os.path.exists(config.backup_path): + if config.strategy == 'preserve': + # Restore preserved items (.config and local_pkgs) + restore_backup(config, config.backup_path, preserve_items=True) + elif config.strategy == 'delete_all': + # Delete backup without restoring + cleanup_backup_directory(config.backup_path) + elif config.strategy == 'backup_all': + # Copy config and hardlink local_pkgs, keep backup + restore_with_hardlink(config, config.backup_path) + + # Step 10: Show next steps + show_next_steps(config) + + return 0 + + except KeyboardInterrupt: + print() + log_info('installation_cancelled') + return 1 + except Exception as e: + print() + log_error('installation_failed', str(e)) + + # Handle backup if installation failed + if config and config.backup_path and os.path.exists(config.backup_path): + handle_installation_failure(config, config.backup_path, config.strategy) + + return 1 + + +def main(): + """Main entry point""" + try: + args = parse_arguments() + # Set language before logging + set_language(args.language) + log_info('start') + result = run_touch_env(args) + sys.exit(result) + except KeyboardInterrupt: + print() + log_info('installation_cancelled') + sys.exit(1) + except Exception as e: + print() + log_error('installation_failed', str(e)) + sys.exit(1) + +# ============================================================================ +if __name__ == '__main__': + main() diff --git a/touch_env.ps1 b/touch_env.ps1 deleted file mode 100644 index c2d3ce99..00000000 --- a/touch_env.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -$DEFAULT_RTT_PACKAGE_URL = "https://github.com/RT-Thread/packages.git" -$ENV_URL = "https://github.com/RT-Thread/env.git" -$SDK_URL = "https://github.com/RT-Thread/sdk.git" - -if ($args[0] -eq "--gitee") { - echo "Using gitee service." - $DEFAULT_RTT_PACKAGE_URL = "https://gitee.com/RT-Thread-Mirror/packages.git" - $ENV_URL = "https://gitee.com/RT-Thread-Mirror/env.git" - $SDK_URL = "https://gitee.com/RT-Thread-Mirror/sdk.git" -} - -$env_dir = "$HOME\.env" - -if (Test-Path -Path $env_dir) { - $option = Read-Host ".env directory already exists. Would you like to remove and recreate .env directory? (Y/N) " option -} if (( $option -eq 'Y' ) -or ($option -eq 'y')) { - Get-ChildItem $env_dir -Recurse | Remove-Item -Force -Recurse - rm -r $env_dir -} - -if (!(Test-Path -Path $env_dir)) { - echo "creating .env folder!" - $package_url = $DEFAULT_RTT_PACKAGE_URL - mkdir $env_dir | Out-Null - mkdir $env_dir\local_pkgs | Out-Null - mkdir $env_dir\packages | Out-Null - mkdir $env_dir\tools | Out-Null - git clone $package_url $env_dir/packages/packages --depth=1 - echo 'source "$PKGS_DIR/packages/Kconfig"' | Out-File -FilePath $env_dir/packages/Kconfig -Encoding ASCII - git clone $SDK_URL $env_dir/packages/sdk --depth=1 - git clone $ENV_URL $env_dir/tools/scripts --depth=1 - copy $env_dir/tools/scripts/env.ps1 $env_dir/env.ps1 -} else { - echo ".env folder has exsited. Jump this step." -} diff --git a/touch_env.py b/touch_env.py deleted file mode 100644 index e69de29b..00000000 diff --git a/touch_env.sh b/touch_env.sh deleted file mode 100755 index c35dc4d8..00000000 --- a/touch_env.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -DEFAULT_RTT_PACKAGE_URL=https://github.com/RT-Thread/packages.git -ENV_URL=https://github.com/RT-Thread/env.git -SDK_URL="https://github.com/RT-Thread/sdk.git" - -if [ $1 ] && [ $1 = --gitee ]; then - gitee=1 - DEFAULT_RTT_PACKAGE_URL=https://gitee.com/RT-Thread-Mirror/packages.git - ENV_URL=https://gitee.com/RT-Thread-Mirror/env.git - SDK_URL="https://gitee.com/RT-Thread-Mirror/sdk.git" -fi - -env_dir=$HOME/.env -if [ -d $env_dir ]; then - read -p '.env directory already exists. Would you like to remove and recreate .env directory? (Y/N) ' option - if [[ "$option" =~ [Yy*] ]]; then - rm -rf $env_dir - fi -fi - -if ! [ -d $env_dir ]; then - package_url=${RTT_PACKAGE_URL:-$DEFAULT_RTT_PACKAGE_URL} - mkdir $env_dir - mkdir $env_dir/local_pkgs - mkdir $env_dir/packages - mkdir $env_dir/tools - git clone $package_url $env_dir/packages/packages --depth=1 - echo 'source "$PKGS_DIR/packages/Kconfig"' >$env_dir/packages/Kconfig - git clone $SDK_URL $env_dir/packages/sdk --depth=1 - git clone $ENV_URL $env_dir/tools/scripts --depth=1 - echo -e 'export PATH=`python3 -m site --user-base`/bin:$HOME/.env/tools/scripts:$PATH\nexport RTT_EXEC_PATH=/usr/bin' >$env_dir/env.sh -fi