shipcli 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
shipcli-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CaffeineOddity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
shipcli-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: shipcli
3
+ Version: 0.0.1
4
+ Summary: Pure Python CLI scaffold and delivery tool.
5
+ Author: CaffeineOddity
6
+ License-Expression: MIT
7
+ Keywords: cli,scaffold,pyinstaller,release,tooling
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: build
20
+ Requires-Dist: pyinstaller>=6.0; extra == "build"
21
+ Provides-Extra: publish
22
+ Requires-Dist: twine>=5.1.0; extra == "publish"
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2.2; extra == "dev"
25
+ Requires-Dist: pytest>=9.0.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # shipcli
29
+
30
+ `shipcli` 是一个纯 Python 的 CLI 脚手架与交付工具,用来初始化、构建、发布、安装和升级命令行项目。
31
+
32
+ ## 特性
33
+
34
+ - 用 `shipcli init <path>` 快速生成新的 Python CLI 项目
35
+ - 用 `shipcli build` / `shipcli build --release` 生成 dev 或 release 版的 wheel/sdist 分发文件
36
+ - 用 `shipcli publish` 把 release 版本发布到 GitHub Release 或 PyPI
37
+ - `shipcli upgrade` / `uninstall` 只管 shipcli 自身;项目 CLI(如 demo-cli)用其自带命令管理
38
+ - 默认以当前目录作为项目目录,也支持 `--project <path>` 跨目录操作
39
+ - 初始化后的项目默认包含 `help`、`version`、`upgrade`、`uninstall` 命令示例
40
+
41
+ ## 命令作用范围
42
+
43
+ | 命令 | 作用对象 |
44
+ |------|---------|
45
+ | `init` / `build` / `install` / `publish` | 目标项目(当前目录或 `--project`,如 demo-cli) |
46
+ | `upgrade` / `uninstall` | shipcli 自身 |
47
+
48
+ 若要升级或卸载某个项目 CLI(如 demo-cli),应先 `install` 安装它,再用其自带命令:`demo-cli upgrade` / `demo-cli uninstall`。
49
+
50
+ ## 安装
51
+
52
+ ### 首次安装(开发)
53
+
54
+ shipcli 未发布到 PyPI 前,从本地仓库安装:
55
+
56
+ ```bash
57
+ git clone <your-repo-url>
58
+ cd shipcli
59
+ python3 -m pip install -e . # editable 安装,改代码即生效
60
+ ```
61
+
62
+ 安装完成后:
63
+
64
+ ```bash
65
+ shipcli --help
66
+ shipcli --version # 形如:shipcli 0.0.1 (editable)
67
+ ```
68
+
69
+ `--version` / `--help` 会标注当前安装形态:`editable`(editable 安装)、`installed`(pip 正式安装)、`binary`(PyInstaller 二进制)。
70
+
71
+ ### 安装 build 产物
72
+
73
+ `shipcli build` 产出 wheel/sdist 后,可直接装本地 wheel(模拟用户从 PyPI 安装):
74
+
75
+ ```bash
76
+ shipcli build --release
77
+ python3 -m pip install .build/dist/0.0.1/dist/*.whl
78
+ ```
79
+
80
+ ## 快速开始
81
+
82
+ ```bash
83
+ shipcli init demo-cli
84
+
85
+ cd demo-cli
86
+ shipcli build # dev 构建,产出 <version>.devN
87
+ shipcli build --release # release 构建,产出发布版 wheel/sdist
88
+ shipcli publish --github --pypi # 发布到 GitHub Release + PyPI
89
+ ```
90
+
91
+ 也支持不切目录,直接指定目标项目:
92
+
93
+ ```bash
94
+ shipcli build --project ./demo-cli
95
+ shipcli publish --project ./demo-cli --github --github-repo owner/demo-cli
96
+ ```
97
+
98
+ ## 构建
99
+
100
+ `shipcli build` 产出标准的 Python 分发文件(wheel/sdist),不再依赖 PyInstaller:
101
+
102
+ - **dev 构建**(`shipcli build`):版本号 `<base>.devN`(如 `0.0.1.dev1`),build 号自动 +1。用于本地验证。
103
+ - **release 构建**(`shipcli build --release`):版本号取配置中的 `version`(如 `0.0.1`)。
104
+ - **版本递增**(`--increase <major|minor|patch>`):构建前把版本号指定位 +1(低位归零)并重置 build,可与 `--release` 同用。
105
+
106
+ 版本号唯一真源是 `build.config.json` 的 `version`;release 构建会自动同步到 `pyproject.toml`,构建时会写入包 `__init__.py` 的 `__version__`。
107
+
108
+ ```bash
109
+ shipcli build # dev 构建
110
+ shipcli build --release # release 构建
111
+ shipcli build --increase patch # patch+1 后 dev 构建
112
+ shipcli build --increase minor --release # minor+1 后 release 构建
113
+ ```
114
+
115
+ 产物目录结构:
116
+
117
+ ```text
118
+ .build/dist/<version>/dist/
119
+ ├── <name>-<version>-py3-none-any.whl
120
+ └── <name>-<version>.tar.gz
121
+ ```
122
+
123
+ ## 升级 shipcli 自身
124
+
125
+ ```bash
126
+ shipcli upgrade # 从 PyPI 升级到最新发布版
127
+ shipcli upgrade --version 0.0.1 # 从 PyPI 升级到指定发布版
128
+ shipcli upgrade --local . # 从本地项目 build 产物升级(取最新版本)
129
+ shipcli upgrade --local . --version 0.0.1.dev1 # 指定本地 build 产物版本
130
+ shipcli uninstall # 卸载 shipcli(pip 卸载 + 清理本地痕迹)
131
+ ```
132
+
133
+ dev 版不上 PyPI,仅本地安装/升级:先 `shipcli build` 产出 dev wheel,再用 `shipcli upgrade --local .` 装上。
134
+
135
+ ## 发布
136
+
137
+ - `shipcli publish` 只做发布,不负责构建
138
+ - 发布前先执行 `shipcli build --release`
139
+ - 发布到 GitHub Release:上传 wheel/sdist 及各自的 `.sha256` 校验文件
140
+ - 发布到 PyPI:上传 wheel/sdist
141
+ - GitHub 凭证只从环境变量读取:`SHIPCLI_GITHUB_TOKEN` 或 `GITHUB_TOKEN`
142
+ - GitHub 仓库可通过 `--github-repo` 传入,或设置 `SHIPCLI_GITHUB_REPO` / `GITHUB_REPOSITORY`
143
+ - PyPI 凭证只从环境变量读取:`SHIPCLI_PYPI_TOKEN`,也兼容现有 `TWINE_PASSWORD`
144
+ - `publish` 只支持 release 版本,不接受 dev 版本
145
+
146
+ ```bash
147
+ cd demo-cli
148
+
149
+ # 发布本地已构建的 release 分发文件到 GitHub Release + PyPI
150
+ shipcli build --release
151
+ SHIPCLI_GITHUB_TOKEN=... SHIPCLI_PYPI_TOKEN=... \
152
+ shipcli publish --github --pypi --github-repo owner/demo-cli
153
+ ```
154
+
155
+ ## 初始化后的目录结构
156
+
157
+ 执行 `shipcli init demo-cli` 后,默认会生成:
158
+
159
+ ```text
160
+ demo-cli/
161
+ ├── build.config.json
162
+ ├── README.md
163
+ ├── demo_cli/
164
+ │ ├── __init__.py
165
+ │ ├── __main__.py
166
+ │ ├── cli.py
167
+ │ └── commands/
168
+ │ ├── __init__.py
169
+ │ ├── help.py
170
+ │ ├── version.py
171
+ │ ├── upgrade.py
172
+ │ └── uninstall.py
173
+ └── tests/
174
+ └── test_cli.py
175
+ ```
176
+
177
+ ## 开发验证
178
+
179
+ ```bash
180
+ python3 -m pytest
181
+ shipcli build --release
182
+ ```
@@ -0,0 +1,155 @@
1
+ # shipcli
2
+
3
+ `shipcli` 是一个纯 Python 的 CLI 脚手架与交付工具,用来初始化、构建、发布、安装和升级命令行项目。
4
+
5
+ ## 特性
6
+
7
+ - 用 `shipcli init <path>` 快速生成新的 Python CLI 项目
8
+ - 用 `shipcli build` / `shipcli build --release` 生成 dev 或 release 版的 wheel/sdist 分发文件
9
+ - 用 `shipcli publish` 把 release 版本发布到 GitHub Release 或 PyPI
10
+ - `shipcli upgrade` / `uninstall` 只管 shipcli 自身;项目 CLI(如 demo-cli)用其自带命令管理
11
+ - 默认以当前目录作为项目目录,也支持 `--project <path>` 跨目录操作
12
+ - 初始化后的项目默认包含 `help`、`version`、`upgrade`、`uninstall` 命令示例
13
+
14
+ ## 命令作用范围
15
+
16
+ | 命令 | 作用对象 |
17
+ |------|---------|
18
+ | `init` / `build` / `install` / `publish` | 目标项目(当前目录或 `--project`,如 demo-cli) |
19
+ | `upgrade` / `uninstall` | shipcli 自身 |
20
+
21
+ 若要升级或卸载某个项目 CLI(如 demo-cli),应先 `install` 安装它,再用其自带命令:`demo-cli upgrade` / `demo-cli uninstall`。
22
+
23
+ ## 安装
24
+
25
+ ### 首次安装(开发)
26
+
27
+ shipcli 未发布到 PyPI 前,从本地仓库安装:
28
+
29
+ ```bash
30
+ git clone <your-repo-url>
31
+ cd shipcli
32
+ python3 -m pip install -e . # editable 安装,改代码即生效
33
+ ```
34
+
35
+ 安装完成后:
36
+
37
+ ```bash
38
+ shipcli --help
39
+ shipcli --version # 形如:shipcli 0.0.1 (editable)
40
+ ```
41
+
42
+ `--version` / `--help` 会标注当前安装形态:`editable`(editable 安装)、`installed`(pip 正式安装)、`binary`(PyInstaller 二进制)。
43
+
44
+ ### 安装 build 产物
45
+
46
+ `shipcli build` 产出 wheel/sdist 后,可直接装本地 wheel(模拟用户从 PyPI 安装):
47
+
48
+ ```bash
49
+ shipcli build --release
50
+ python3 -m pip install .build/dist/0.0.1/dist/*.whl
51
+ ```
52
+
53
+ ## 快速开始
54
+
55
+ ```bash
56
+ shipcli init demo-cli
57
+
58
+ cd demo-cli
59
+ shipcli build # dev 构建,产出 <version>.devN
60
+ shipcli build --release # release 构建,产出发布版 wheel/sdist
61
+ shipcli publish --github --pypi # 发布到 GitHub Release + PyPI
62
+ ```
63
+
64
+ 也支持不切目录,直接指定目标项目:
65
+
66
+ ```bash
67
+ shipcli build --project ./demo-cli
68
+ shipcli publish --project ./demo-cli --github --github-repo owner/demo-cli
69
+ ```
70
+
71
+ ## 构建
72
+
73
+ `shipcli build` 产出标准的 Python 分发文件(wheel/sdist),不再依赖 PyInstaller:
74
+
75
+ - **dev 构建**(`shipcli build`):版本号 `<base>.devN`(如 `0.0.1.dev1`),build 号自动 +1。用于本地验证。
76
+ - **release 构建**(`shipcli build --release`):版本号取配置中的 `version`(如 `0.0.1`)。
77
+ - **版本递增**(`--increase <major|minor|patch>`):构建前把版本号指定位 +1(低位归零)并重置 build,可与 `--release` 同用。
78
+
79
+ 版本号唯一真源是 `build.config.json` 的 `version`;release 构建会自动同步到 `pyproject.toml`,构建时会写入包 `__init__.py` 的 `__version__`。
80
+
81
+ ```bash
82
+ shipcli build # dev 构建
83
+ shipcli build --release # release 构建
84
+ shipcli build --increase patch # patch+1 后 dev 构建
85
+ shipcli build --increase minor --release # minor+1 后 release 构建
86
+ ```
87
+
88
+ 产物目录结构:
89
+
90
+ ```text
91
+ .build/dist/<version>/dist/
92
+ ├── <name>-<version>-py3-none-any.whl
93
+ └── <name>-<version>.tar.gz
94
+ ```
95
+
96
+ ## 升级 shipcli 自身
97
+
98
+ ```bash
99
+ shipcli upgrade # 从 PyPI 升级到最新发布版
100
+ shipcli upgrade --version 0.0.1 # 从 PyPI 升级到指定发布版
101
+ shipcli upgrade --local . # 从本地项目 build 产物升级(取最新版本)
102
+ shipcli upgrade --local . --version 0.0.1.dev1 # 指定本地 build 产物版本
103
+ shipcli uninstall # 卸载 shipcli(pip 卸载 + 清理本地痕迹)
104
+ ```
105
+
106
+ dev 版不上 PyPI,仅本地安装/升级:先 `shipcli build` 产出 dev wheel,再用 `shipcli upgrade --local .` 装上。
107
+
108
+ ## 发布
109
+
110
+ - `shipcli publish` 只做发布,不负责构建
111
+ - 发布前先执行 `shipcli build --release`
112
+ - 发布到 GitHub Release:上传 wheel/sdist 及各自的 `.sha256` 校验文件
113
+ - 发布到 PyPI:上传 wheel/sdist
114
+ - GitHub 凭证只从环境变量读取:`SHIPCLI_GITHUB_TOKEN` 或 `GITHUB_TOKEN`
115
+ - GitHub 仓库可通过 `--github-repo` 传入,或设置 `SHIPCLI_GITHUB_REPO` / `GITHUB_REPOSITORY`
116
+ - PyPI 凭证只从环境变量读取:`SHIPCLI_PYPI_TOKEN`,也兼容现有 `TWINE_PASSWORD`
117
+ - `publish` 只支持 release 版本,不接受 dev 版本
118
+
119
+ ```bash
120
+ cd demo-cli
121
+
122
+ # 发布本地已构建的 release 分发文件到 GitHub Release + PyPI
123
+ shipcli build --release
124
+ SHIPCLI_GITHUB_TOKEN=... SHIPCLI_PYPI_TOKEN=... \
125
+ shipcli publish --github --pypi --github-repo owner/demo-cli
126
+ ```
127
+
128
+ ## 初始化后的目录结构
129
+
130
+ 执行 `shipcli init demo-cli` 后,默认会生成:
131
+
132
+ ```text
133
+ demo-cli/
134
+ ├── build.config.json
135
+ ├── README.md
136
+ ├── demo_cli/
137
+ │ ├── __init__.py
138
+ │ ├── __main__.py
139
+ │ ├── cli.py
140
+ │ └── commands/
141
+ │ ├── __init__.py
142
+ │ ├── help.py
143
+ │ ├── version.py
144
+ │ ├── upgrade.py
145
+ │ └── uninstall.py
146
+ └── tests/
147
+ └── test_cli.py
148
+ ```
149
+
150
+ ## 开发验证
151
+
152
+ ```bash
153
+ python3 -m pytest
154
+ shipcli build --release
155
+ ```
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shipcli"
7
+ version = "0.0.1"
8
+ description = "Pure Python CLI scaffold and delivery tool."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "CaffeineOddity" }
15
+ ]
16
+ keywords = ["cli", "scaffold", "pyinstaller", "release", "tooling"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: MacOS",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Topic :: Software Development :: Build Tools",
25
+ "Topic :: Utilities"
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.optional-dependencies]
30
+ build = [
31
+ "pyinstaller>=6.0"
32
+ ]
33
+ publish = [
34
+ "twine>=5.1.0"
35
+ ]
36
+ dev = [
37
+ "build>=1.2.2",
38
+ "pytest>=9.0.0"
39
+ ]
40
+
41
+ [project.scripts]
42
+ shipcli = "shipcli.cli:main"
43
+
44
+ [tool.setuptools.packages.find]
45
+ include = ["shipcli*"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """shipcli 包入口。"""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.1"
@@ -0,0 +1,11 @@
1
+ """shipcli 模块入口。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from shipcli.cli import main
8
+
9
+
10
+ if __name__ == "__main__":
11
+ sys.exit(main())
@@ -0,0 +1,220 @@
1
+ """shipcli 纯 Python 构建流程。
2
+
3
+ 方向 A:build 产出标准的 wheel/sdist(去掉 PyInstaller 二进制)。
4
+ - debug 构建(`shipcli build`)产 dev 版 wheel,版本号 `<base>.devN`。
5
+ - release 构建(`shipcli build --release`)产 release 版 wheel/sdist。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from shipcli.config import BuildConfig, config_path, load_config
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class BuildPlan:
23
+ """描述一次构建的关键路径与版本信息。"""
24
+
25
+ app_name: str
26
+ build_root: Path
27
+ dist_root: Path
28
+ product_dir: Path
29
+ dist_dir: Path
30
+ product_version: str
31
+ base_version: str
32
+ build_number: int
33
+ release: bool
34
+
35
+
36
+ def bump_version(version: str, part: str) -> str:
37
+ """把语义版本号的指定位 +1,低位归零。"""
38
+ if part not in {"major", "minor", "patch"}:
39
+ raise ValueError("版本位仅支持 major / minor / patch")
40
+ segments = version.split(".")
41
+ if len(segments) != 3 or not all(seg.isdigit() for seg in segments):
42
+ raise ValueError(f"版本号不是标准的三段语义版本:{version}")
43
+ major, minor, patch = (int(seg) for seg in segments)
44
+ if part == "major":
45
+ major, minor, patch = major + 1, 0, 0
46
+ elif part == "minor":
47
+ minor, patch = minor + 1, 0
48
+ else:
49
+ patch += 1
50
+ return f"{major}.{minor}.{patch}"
51
+
52
+
53
+ def resolve_build_plan(
54
+ project_root: Path,
55
+ config: BuildConfig,
56
+ release: bool = False,
57
+ increase: str | None = None,
58
+ ) -> BuildPlan:
59
+ """根据配置和参数生成本次构建计划。"""
60
+ if increase is not None:
61
+ base_version = bump_version(config["version"], increase)
62
+ else:
63
+ base_version = config["version"]
64
+
65
+ build_root = project_root / config["build_root"]
66
+ dist_root = build_root / "dist"
67
+ if release:
68
+ build_number = config["build"]
69
+ product_version = base_version
70
+ else:
71
+ build_number = config["build"] + 1
72
+ # PEP 440 规范的 dev 版本号:`<base>.devN`(可被 pip/PyPI 接受)。
73
+ product_version = f"{base_version}.dev{build_number}"
74
+
75
+ product_dir = dist_root / product_version
76
+ dist_dir = product_dir / "dist"
77
+ return BuildPlan(
78
+ app_name=config["app_name"],
79
+ build_root=build_root,
80
+ dist_root=dist_root,
81
+ product_dir=product_dir,
82
+ dist_dir=dist_dir,
83
+ product_version=product_version,
84
+ base_version=base_version,
85
+ build_number=build_number,
86
+ release=release,
87
+ )
88
+
89
+
90
+ def run_build(
91
+ project_root: Path,
92
+ release: bool = False,
93
+ increase: str | None = None,
94
+ ) -> BuildPlan:
95
+ """生成本次构建的 wheel/sdist,并在 debug 构建成功后回写 build。"""
96
+ config = load_config(project_root)
97
+ if not (project_root / "pyproject.toml").is_file():
98
+ raise FileNotFoundError("未找到 pyproject.toml,无法构建分发文件")
99
+ if shutil_is_missing_python():
100
+ raise RuntimeError("未找到可用的 Python 解释器")
101
+
102
+ # --increase 先把 bump 后的版本与重置的 build=0 回写配置,再据此生成计划,
103
+ # 保证后续 dev 号、产物目录都基于新版本与新 build 号。
104
+ if increase is not None:
105
+ config["version"] = bump_version(config["version"], increase)
106
+ config["build"] = 0
107
+ _write_version_and_build(project_root, config["version"], config["build"])
108
+
109
+ plan = resolve_build_plan(project_root, config, release=release, increase=None)
110
+
111
+ plan.build_root.mkdir(parents=True, exist_ok=True)
112
+ if plan.product_dir.exists():
113
+ import shutil
114
+
115
+ shutil.rmtree(plan.product_dir)
116
+ plan.product_dir.mkdir(parents=True, exist_ok=True)
117
+
118
+ # release 构建需把版本同步到 pyproject.toml,再产 wheel/sdist;
119
+ # dev 构建也要把 dev 版号同步进 pyproject.toml,否则 wheel 版本号不对。
120
+ target_version = plan.base_version if plan.release else plan.product_version
121
+ _sync_pyproject_version(project_root, target_version)
122
+ _write_package_version(project_root, config, plan.product_version)
123
+ _build_distributions(project_root, plan)
124
+
125
+ if not plan.release:
126
+ _write_build_number(project_root, plan.build_number)
127
+ else:
128
+ # release 构建会改写版本相关文件,git 仓库下自动提交,
129
+ # 保证后续 publish 打的 tag 落在「版本号已更新」的完整 commit 上。
130
+ _maybe_commit_release(project_root, plan.base_version)
131
+ return plan
132
+
133
+
134
+ def _maybe_commit_release(project_root: Path, version: str) -> None:
135
+ """release 构建后,若为 git 仓库且有改动,自动提交版本发布点。"""
136
+ from shipcli.publisher import git_commit_all, git_working_tree_clean, is_git_repo
137
+
138
+ if not is_git_repo(project_root):
139
+ return
140
+ if git_working_tree_clean(project_root):
141
+ return
142
+ git_commit_all(project_root, f"release v{version}")
143
+
144
+
145
+ def shutil_is_missing_python() -> bool:
146
+ """检测当前解释器是否可用(保留可测试的薄封装)。"""
147
+ import shutil
148
+
149
+ return shutil.which(sys.executable) is None
150
+
151
+
152
+ def _build_distributions(project_root: Path, plan: BuildPlan) -> None:
153
+ """调用 `python -m build` 生成 wheel/sdist 到 plan.dist_dir。"""
154
+ if importlib.util.find_spec("build") is None:
155
+ raise RuntimeError("未安装 build,请先执行 `python3 -m pip install build`")
156
+
157
+ plan.dist_dir.mkdir(parents=True, exist_ok=True)
158
+ command = [
159
+ sys.executable,
160
+ "-m",
161
+ "build",
162
+ "--outdir",
163
+ str(plan.dist_dir),
164
+ str(project_root),
165
+ ]
166
+ subprocess.run(command, check=True, cwd=project_root)
167
+ if not any(plan.dist_dir.glob("*.whl")):
168
+ raise RuntimeError(f"分发文件构建失败:{plan.dist_dir}")
169
+
170
+
171
+ def _package_init_path(project_root: Path, config: BuildConfig) -> Path:
172
+ """根据构建入口推导包根目录下的 __init__.py 路径。"""
173
+ entry = config["entry"]
174
+ package = entry.split("/__main__.py", 1)[0] if entry.endswith("/__main__.py") else entry.rsplit("/", 1)[0]
175
+ return project_root / package / "__init__.py"
176
+
177
+
178
+ def _write_package_version(project_root: Path, config: BuildConfig, version: str) -> None:
179
+ """把本次构建版本写入包 __init__.py 的 __version__,供打包后运行时读取。"""
180
+ path = _package_init_path(project_root, config)
181
+ if not path.is_file():
182
+ raise FileNotFoundError(f"包入口文件不存在:{path}")
183
+ text = path.read_text(encoding="utf-8")
184
+ pattern = re.compile(r'(?m)^(__version__\s*=\s*")([^"]*)("\s*)$')
185
+ if not pattern.search(text):
186
+ raise RuntimeError(f"{path} 缺少 __version__ 定义,无法写入版本号")
187
+ synced = pattern.sub(lambda m: f'{m.group(1)}{version}{m.group(3)}', text)
188
+ if synced != text:
189
+ path.write_text(synced, encoding="utf-8")
190
+
191
+
192
+ def _sync_pyproject_version(project_root: Path, version: str) -> None:
193
+ """把目标版本同步到 pyproject.toml,保证 wheel 版本号正确。"""
194
+ path = project_root / "pyproject.toml"
195
+ if not path.is_file():
196
+ return
197
+ text = path.read_text(encoding="utf-8")
198
+ pattern = re.compile(r'(?m)^(\s*version\s*=\s*")([^"]*)("\s*)$')
199
+ if not pattern.search(text):
200
+ return # 静态版本号缺失时跳过(可能用了动态版本,不强行改写)
201
+ synced = pattern.sub(lambda m: f'{m.group(1)}{version}{m.group(3)}', text)
202
+ if synced != text:
203
+ path.write_text(synced, encoding="utf-8")
204
+
205
+
206
+ def _write_version_and_build(project_root: Path, version: str, build: int) -> None:
207
+ """更新 build.config.json 中的 version 与 build 字段。"""
208
+ path = config_path(project_root)
209
+ payload = json.loads(path.read_text(encoding="utf-8"))
210
+ payload["version"] = version
211
+ payload["build"] = build
212
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
213
+
214
+
215
+ def _write_build_number(project_root: Path, build_number: int) -> None:
216
+ """只更新 build.config.json 中的 build 字段。"""
217
+ path = config_path(project_root)
218
+ payload = json.loads(path.read_text(encoding="utf-8"))
219
+ payload["build"] = build_number
220
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")