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)
+[](https://www.python.org/)
+[]()
+
+**[简体中文](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 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)
+
+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.)
+[](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)
+[](https://www.python.org/)
+[]()
+
+**[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 仓库|[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** 许可证开源。
+
+---
+
+
+
+## 🤝 贡献者
+
+感谢所有为 RT-Thread Env 项目做出贡献的开发者!
+
+[](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