pyi-doctor 1.0.0__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.
- pyi_doctor-1.0.0/LICENSE +21 -0
- pyi_doctor-1.0.0/PKG-INFO +195 -0
- pyi_doctor-1.0.0/README.md +163 -0
- pyi_doctor-1.0.0/pyproject.toml +52 -0
- pyi_doctor-1.0.0/setup.cfg +4 -0
- pyi_doctor-1.0.0/src/pyi_doctor/__init__.py +74 -0
- pyi_doctor-1.0.0/src/pyi_doctor/__main__.py +5 -0
- pyi_doctor-1.0.0/src/pyi_doctor/_proc.py +55 -0
- pyi_doctor-1.0.0/src/pyi_doctor/antivirus.py +232 -0
- pyi_doctor-1.0.0/src/pyi_doctor/cli.py +183 -0
- pyi_doctor-1.0.0/src/pyi_doctor/config.py +281 -0
- pyi_doctor-1.0.0/src/pyi_doctor/errors.py +30 -0
- pyi_doctor-1.0.0/src/pyi_doctor/installer.py +193 -0
- pyi_doctor-1.0.0/src/pyi_doctor/licenses.py +276 -0
- pyi_doctor-1.0.0/src/pyi_doctor/pe.py +186 -0
- pyi_doctor-1.0.0/src/pyi_doctor/pipeline.py +238 -0
- pyi_doctor-1.0.0/src/pyi_doctor/portable.py +121 -0
- pyi_doctor-1.0.0/src/pyi_doctor/release.py +104 -0
- pyi_doctor-1.0.0/src/pyi_doctor/report.py +154 -0
- pyi_doctor-1.0.0/src/pyi_doctor/sbom.py +84 -0
- pyi_doctor-1.0.0/src/pyi_doctor/smoke.py +294 -0
- pyi_doctor-1.0.0/src/pyi_doctor/versioninfo.py +56 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/PKG-INFO +195 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/SOURCES.txt +34 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/dependency_links.txt +1 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/entry_points.txt +2 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/requires.txt +7 -0
- pyi_doctor-1.0.0/src/pyi_doctor.egg-info/top_level.txt +1 -0
- pyi_doctor-1.0.0/tests/test_config.py +143 -0
- pyi_doctor-1.0.0/tests/test_integration.py +50 -0
- pyi_doctor-1.0.0/tests/test_licenses_sbom.py +117 -0
- pyi_doctor-1.0.0/tests/test_pe_antivirus.py +142 -0
- pyi_doctor-1.0.0/tests/test_pipeline_cli.py +176 -0
- pyi_doctor-1.0.0/tests/test_release_report.py +108 -0
- pyi_doctor-1.0.0/tests/test_smoke.py +138 -0
- pyi_doctor-1.0.0/tests/test_versioninfo_iss.py +156 -0
pyi_doctor-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 서주원 (OMEGA SYSTEMS)
|
|
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.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyi-doctor
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Build PyInstaller apps as portable exe + installer, then check startup, antivirus risk factors and bundled licenses.
|
|
5
|
+
Author-email: "서주원 (OMEGA SYSTEMS)" <seodes105@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sjw0328/pyi-doctor
|
|
8
|
+
Project-URL: Issues, https://github.com/sjw0328/pyi-doctor/issues
|
|
9
|
+
Keywords: pyinstaller,installer,inno-setup,antivirus,false-positive,sbom,windows,packaging
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Classifier: Topic :: System :: Software Distribution
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: pyinstaller>=6.0
|
|
28
|
+
Requires-Dist: tomli>=1.1; python_version < "3.11"
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# pyi-doctor
|
|
34
|
+
|
|
35
|
+
[한국어 문서 (Korean)](README.ko.md)
|
|
36
|
+
|
|
37
|
+
**Build a PyInstaller app as a portable exe and a setup wizard in one call, then check whether it starts outside your machine, which antivirus false-positive risk factors it carries, and which third-party licenses it bundles.**
|
|
38
|
+
|
|
39
|
+
Tools such as briefcase and pyship focus on shipping an app. pyi-doctor focuses on the problems that tend to show up after shipping a PyInstaller build: "it works on my PC only", "Windows Defender calls it a trojan", and "which licenses am I redistributing?".
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pyi_doctor import Project, build_all
|
|
43
|
+
|
|
44
|
+
project = Project(entry="main.py", name="MyApp", version="1.0.0", publisher="Me")
|
|
45
|
+
report = build_all(project, log=print)
|
|
46
|
+
print(report.ok)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
dist/release/
|
|
51
|
+
├── MyApp-1.0.0-portable-win64.zip # onedir build (+ THIRD_PARTY_LICENSES.txt)
|
|
52
|
+
├── MyApp-1.0.0-win64.exe # single-file build (if onefile = true)
|
|
53
|
+
├── MyApp-1.0.0-setup.exe # Inno Setup wizard (Windows + Inno Setup)
|
|
54
|
+
├── MyApp-1.0.0.cdx.json # CycloneDX 1.5 SBOM
|
|
55
|
+
├── THIRD_PARTY_LICENSES.txt
|
|
56
|
+
├── SHA256SUMS.txt
|
|
57
|
+
├── av-submission-notes.txt # text for false-positive submission forms
|
|
58
|
+
├── report.md / report.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What it does
|
|
62
|
+
|
|
63
|
+
| Step | What happens |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Portable build | PyInstaller **onedir** build (default) with `--noupx`, a Windows version resource and your icon. Optional **onefile** build. |
|
|
66
|
+
| Installer | Writes an Inno Setup script (fixed AppId for upgrades, per-user install by default, modern wizard, Start menu / optional desktop shortcut, uninstaller, English + Korean and other languages) and compiles it when Inno Setup 6.3+ is installed. |
|
|
67
|
+
| Smoke test | Copies each build to a temp folder and starts it with Python's environment variables and folders removed from the environment. Reports missing modules, data files and DLL errors with hints (e.g. "add 'x' to hidden_imports"). |
|
|
68
|
+
| Antivirus diagnosis | Reads the PE file (pure Python) and scores commonly cited risk factors: UPX sections, onefile, missing version resource, no signature, prebuilt bootloader, default icon. On Windows it can also scan with the local Microsoft Defender CLI (no remediation). |
|
|
69
|
+
| Licenses + SBOM | Maps bundled modules (from PyInstaller's TOC files) to installed distributions and writes `THIRD_PARTY_LICENSES.txt` (also placed next to the exe and inside the installer) and a CycloneDX SBOM. |
|
|
70
|
+
| Release folder | Collects the files, writes `SHA256SUMS.txt`, a report and false-positive submission notes. Optional upload to a GitHub release through the GitHub CLI (`gh`). |
|
|
71
|
+
|
|
72
|
+
Only the onedir build is required to succeed; every other step records its own status, so a missing tool (for example Inno Setup) does not stop the rest.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install pyi-doctor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Requirements: Python 3.9+, PyInstaller 6+ (installed as a dependency). The installer step needs Windows and [Inno Setup](https://jrsoftware.org/isinfo.php) 6.3 or newer (not bundled; set `PYI_DOCTOR_ISCC` if it is not in the default location). The GitHub upload needs the [GitHub CLI](https://cli.github.com/).
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Add a table to your `pyproject.toml` (`pyi-doctor init --write` adds a template):
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[tool.pyi-doctor]
|
|
88
|
+
entry = "main.py"
|
|
89
|
+
name = "MyApp"
|
|
90
|
+
version = "1.0.0"
|
|
91
|
+
publisher = "Your Name"
|
|
92
|
+
description = "What the app does"
|
|
93
|
+
icon = "assets/app.ico"
|
|
94
|
+
app_id = "PUT-A-FIXED-GUID-HERE" # keep it fixed after the first release
|
|
95
|
+
console = false
|
|
96
|
+
onefile = false
|
|
97
|
+
license_file = "LICENSE" # shown in the installer
|
|
98
|
+
add_data = ["assets=assets"]
|
|
99
|
+
hidden_imports = []
|
|
100
|
+
languages = ["english", "korean"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`name`, `version` and `description` fall back to `[project]`. If `app_id` is not set, a GUID is derived from publisher and name; changing either then changes the GUID, so set it explicitly before the first release.
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>All settings</summary>
|
|
107
|
+
|
|
108
|
+
| Key | Default | Meaning |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `entry` | required | Script PyInstaller starts from |
|
|
111
|
+
| `name` | required | Exe / installer name |
|
|
112
|
+
| `version` | `0.1.0` | Version string; the first four numbers go into Windows version resources |
|
|
113
|
+
| `publisher`, `description`, `copyright`, `website` | `""` | Metadata for version resources, installer and reports |
|
|
114
|
+
| `icon` | none | `.ico` file |
|
|
115
|
+
| `app_id` | derived | Installer GUID |
|
|
116
|
+
| `console` | `false` | `true` for command-line apps |
|
|
117
|
+
| `onefile` | `false` | Also build a single-file exe |
|
|
118
|
+
| `upx` | `false` | Allow UPX (adds a high-weight risk factor) |
|
|
119
|
+
| `hidden_imports`, `excludes` | `[]` | Passed to PyInstaller |
|
|
120
|
+
| `add_data` | `[]` | `"src=dest"`, `"src;dest"` or `["src", "dest"]` |
|
|
121
|
+
| `pyinstaller_args` | `[]` | Extra PyInstaller arguments |
|
|
122
|
+
| `license_file` | none | License page in the installer |
|
|
123
|
+
| `privileges` | `lowest` | `lowest` (per-user, can be raised in the wizard) or `admin` |
|
|
124
|
+
| `languages` | `["english", "korean"]` | Installer languages available in your Inno Setup |
|
|
125
|
+
| `smoke_args`, `smoke_timeout` | `[]`, `15` | Arguments and seconds for the smoke test |
|
|
126
|
+
| `output_dir`, `work_dir` | `dist`, `build` | Output folders |
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
## Command line
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pyi-doctor build # full pipeline from pyproject.toml
|
|
134
|
+
pyi-doctor build --onefile --no-defender
|
|
135
|
+
pyi-doctor build --github-release v1.0.0 --github-repo me/myapp
|
|
136
|
+
pyi-doctor diagnose dist/portable/MyApp/MyApp.exe # score any exe
|
|
137
|
+
pyi-doctor smoke dist/portable/MyApp/MyApp.exe -- --version
|
|
138
|
+
pyi-doctor iss # write the Inno Setup script only
|
|
139
|
+
pyi-doctor init --write # add [tool.pyi-doctor] to pyproject.toml
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Library API
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from pyi_doctor import Project, build_portable, build_installer, smoke_test, diagnose, collect_licenses
|
|
146
|
+
|
|
147
|
+
project = Project.from_pyproject("pyproject.toml")
|
|
148
|
+
onedir = build_portable(project) # Artifact(kind="onedir", path=..., folder=...)
|
|
149
|
+
result = smoke_test(onedir.path, timeout=10) # SmokeResult(ok, status, missing_modules, ...)
|
|
150
|
+
risk = diagnose(onedir.path, icon_configured=True) # Diagnosis(score, level, factors, defender, ...)
|
|
151
|
+
setup = build_installer(project, onedir.folder) # Windows + Inno Setup
|
|
152
|
+
packages, mode = collect_licenses(project.work_path / "onedir")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`build_all(project, onefile=..., installer=..., smoke_test=..., diagnose=..., defender=..., licenses=..., sbom_file=..., github_tag=...)` runs everything and returns a `Report` with `.ok`, `.steps`, `.to_markdown()` and `.to_json()`.
|
|
156
|
+
|
|
157
|
+
## About the antivirus score
|
|
158
|
+
|
|
159
|
+
The score adds fixed weights for factors that are commonly reported to affect heuristic detection of PyInstaller executables (UPX 30, onefile 20, no version resource 15, unsigned 15, prebuilt bootloader 10, default icon 5). It is meant for comparing your own builds. It does not predict the verdict of Microsoft Defender or any other product, and a low score does not mean a file will not be flagged.
|
|
160
|
+
|
|
161
|
+
pyi-doctor does not obfuscate, encrypt or otherwise hide code from scanners. It applies ordinary distribution practices (version information, no packer, onedir layout, signing advice) and helps you prepare a false-positive report. Code signing with a certificate from a trusted authority is generally the most effective measure and is outside the scope of this tool.
|
|
162
|
+
|
|
163
|
+
## Measured on the bundled example
|
|
164
|
+
|
|
165
|
+
`examples/hello`, Linux x86_64, Python 3.13, PyInstaller 6.22.3 (installer step skipped because it needs Windows):
|
|
166
|
+
|
|
167
|
+
| Step | Time |
|
|
168
|
+
|---|---|
|
|
169
|
+
| onedir build | 4.5 s |
|
|
170
|
+
| onefile build | 12.2 s |
|
|
171
|
+
| licenses + SBOM | 0.8 s |
|
|
172
|
+
| smoke test (both builds) | 0.6 s |
|
|
173
|
+
| diagnosis | < 0.1 s |
|
|
174
|
+
| release folder (zip + checksums) | 1.5 s |
|
|
175
|
+
| **total** | **19.7 s** |
|
|
176
|
+
|
|
177
|
+
Test suite: 138 tests (including 2 real PyInstaller builds) on Python 3.9, 3.11 and 3.13 on Linux. Windows-specific paths (version resource, Inno Setup compilation, Defender scan, error-dialog detection in the smoke test) run in the Windows CI jobs.
|
|
178
|
+
|
|
179
|
+
## Known limitations
|
|
180
|
+
|
|
181
|
+
- Windows features (installer, version resource, Defender scan) need Windows. On other systems the `.iss` script is still written.
|
|
182
|
+
- The smoke test is a heuristic. It removes Python from the environment but does not reproduce a clean PC (installed DLLs, Visual C++ runtime, fonts, etc.). GUI apps that keep running are counted as started. Windowed apps that crash show a PyInstaller error dialog; on Windows pyi-doctor looks for that dialog by window title, which may not catch every case.
|
|
183
|
+
- The bootloader check compares the installed PyInstaller bootloader with the hashes recorded in its wheel. It describes the PyInstaller in the current environment, not an exe built elsewhere.
|
|
184
|
+
- License notices come from package metadata, which can be missing or wrong; packages loaded dynamically may be missed. Review the generated file.
|
|
185
|
+
- Only PyInstaller is supported (no Nuitka / cx_Freeze). No code signing and no auto-update.
|
|
186
|
+
|
|
187
|
+
## Disclaimer
|
|
188
|
+
|
|
189
|
+
pyi-doctor is rule-based and may miss problems or report problems that do not exist. Its reports are informational and do not replace your own testing, review of licenses, or obligations under applicable law; checking the results and complying with license terms and laws remain the user's responsibility. The software is provided "AS IS", without warranty of any kind, under the MIT License.
|
|
190
|
+
|
|
191
|
+
pyi-doctor is an independent project and is not affiliated with or endorsed by the PyInstaller project, Jordan Russell / Inno Setup, Microsoft, or GitHub. Product names are trademarks of their respective owners.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT © 2026 서주원 (OMEGA SYSTEMS) · Contact: seodes105@gmail.com
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# pyi-doctor
|
|
2
|
+
|
|
3
|
+
[한국어 문서 (Korean)](README.ko.md)
|
|
4
|
+
|
|
5
|
+
**Build a PyInstaller app as a portable exe and a setup wizard in one call, then check whether it starts outside your machine, which antivirus false-positive risk factors it carries, and which third-party licenses it bundles.**
|
|
6
|
+
|
|
7
|
+
Tools such as briefcase and pyship focus on shipping an app. pyi-doctor focuses on the problems that tend to show up after shipping a PyInstaller build: "it works on my PC only", "Windows Defender calls it a trojan", and "which licenses am I redistributing?".
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pyi_doctor import Project, build_all
|
|
11
|
+
|
|
12
|
+
project = Project(entry="main.py", name="MyApp", version="1.0.0", publisher="Me")
|
|
13
|
+
report = build_all(project, log=print)
|
|
14
|
+
print(report.ok)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
dist/release/
|
|
19
|
+
├── MyApp-1.0.0-portable-win64.zip # onedir build (+ THIRD_PARTY_LICENSES.txt)
|
|
20
|
+
├── MyApp-1.0.0-win64.exe # single-file build (if onefile = true)
|
|
21
|
+
├── MyApp-1.0.0-setup.exe # Inno Setup wizard (Windows + Inno Setup)
|
|
22
|
+
├── MyApp-1.0.0.cdx.json # CycloneDX 1.5 SBOM
|
|
23
|
+
├── THIRD_PARTY_LICENSES.txt
|
|
24
|
+
├── SHA256SUMS.txt
|
|
25
|
+
├── av-submission-notes.txt # text for false-positive submission forms
|
|
26
|
+
├── report.md / report.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
| Step | What happens |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Portable build | PyInstaller **onedir** build (default) with `--noupx`, a Windows version resource and your icon. Optional **onefile** build. |
|
|
34
|
+
| Installer | Writes an Inno Setup script (fixed AppId for upgrades, per-user install by default, modern wizard, Start menu / optional desktop shortcut, uninstaller, English + Korean and other languages) and compiles it when Inno Setup 6.3+ is installed. |
|
|
35
|
+
| Smoke test | Copies each build to a temp folder and starts it with Python's environment variables and folders removed from the environment. Reports missing modules, data files and DLL errors with hints (e.g. "add 'x' to hidden_imports"). |
|
|
36
|
+
| Antivirus diagnosis | Reads the PE file (pure Python) and scores commonly cited risk factors: UPX sections, onefile, missing version resource, no signature, prebuilt bootloader, default icon. On Windows it can also scan with the local Microsoft Defender CLI (no remediation). |
|
|
37
|
+
| Licenses + SBOM | Maps bundled modules (from PyInstaller's TOC files) to installed distributions and writes `THIRD_PARTY_LICENSES.txt` (also placed next to the exe and inside the installer) and a CycloneDX SBOM. |
|
|
38
|
+
| Release folder | Collects the files, writes `SHA256SUMS.txt`, a report and false-positive submission notes. Optional upload to a GitHub release through the GitHub CLI (`gh`). |
|
|
39
|
+
|
|
40
|
+
Only the onedir build is required to succeed; every other step records its own status, so a missing tool (for example Inno Setup) does not stop the rest.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install pyi-doctor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requirements: Python 3.9+, PyInstaller 6+ (installed as a dependency). The installer step needs Windows and [Inno Setup](https://jrsoftware.org/isinfo.php) 6.3 or newer (not bundled; set `PYI_DOCTOR_ISCC` if it is not in the default location). The GitHub upload needs the [GitHub CLI](https://cli.github.com/).
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Add a table to your `pyproject.toml` (`pyi-doctor init --write` adds a template):
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
[tool.pyi-doctor]
|
|
56
|
+
entry = "main.py"
|
|
57
|
+
name = "MyApp"
|
|
58
|
+
version = "1.0.0"
|
|
59
|
+
publisher = "Your Name"
|
|
60
|
+
description = "What the app does"
|
|
61
|
+
icon = "assets/app.ico"
|
|
62
|
+
app_id = "PUT-A-FIXED-GUID-HERE" # keep it fixed after the first release
|
|
63
|
+
console = false
|
|
64
|
+
onefile = false
|
|
65
|
+
license_file = "LICENSE" # shown in the installer
|
|
66
|
+
add_data = ["assets=assets"]
|
|
67
|
+
hidden_imports = []
|
|
68
|
+
languages = ["english", "korean"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`name`, `version` and `description` fall back to `[project]`. If `app_id` is not set, a GUID is derived from publisher and name; changing either then changes the GUID, so set it explicitly before the first release.
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary>All settings</summary>
|
|
75
|
+
|
|
76
|
+
| Key | Default | Meaning |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `entry` | required | Script PyInstaller starts from |
|
|
79
|
+
| `name` | required | Exe / installer name |
|
|
80
|
+
| `version` | `0.1.0` | Version string; the first four numbers go into Windows version resources |
|
|
81
|
+
| `publisher`, `description`, `copyright`, `website` | `""` | Metadata for version resources, installer and reports |
|
|
82
|
+
| `icon` | none | `.ico` file |
|
|
83
|
+
| `app_id` | derived | Installer GUID |
|
|
84
|
+
| `console` | `false` | `true` for command-line apps |
|
|
85
|
+
| `onefile` | `false` | Also build a single-file exe |
|
|
86
|
+
| `upx` | `false` | Allow UPX (adds a high-weight risk factor) |
|
|
87
|
+
| `hidden_imports`, `excludes` | `[]` | Passed to PyInstaller |
|
|
88
|
+
| `add_data` | `[]` | `"src=dest"`, `"src;dest"` or `["src", "dest"]` |
|
|
89
|
+
| `pyinstaller_args` | `[]` | Extra PyInstaller arguments |
|
|
90
|
+
| `license_file` | none | License page in the installer |
|
|
91
|
+
| `privileges` | `lowest` | `lowest` (per-user, can be raised in the wizard) or `admin` |
|
|
92
|
+
| `languages` | `["english", "korean"]` | Installer languages available in your Inno Setup |
|
|
93
|
+
| `smoke_args`, `smoke_timeout` | `[]`, `15` | Arguments and seconds for the smoke test |
|
|
94
|
+
| `output_dir`, `work_dir` | `dist`, `build` | Output folders |
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
## Command line
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pyi-doctor build # full pipeline from pyproject.toml
|
|
102
|
+
pyi-doctor build --onefile --no-defender
|
|
103
|
+
pyi-doctor build --github-release v1.0.0 --github-repo me/myapp
|
|
104
|
+
pyi-doctor diagnose dist/portable/MyApp/MyApp.exe # score any exe
|
|
105
|
+
pyi-doctor smoke dist/portable/MyApp/MyApp.exe -- --version
|
|
106
|
+
pyi-doctor iss # write the Inno Setup script only
|
|
107
|
+
pyi-doctor init --write # add [tool.pyi-doctor] to pyproject.toml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Library API
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from pyi_doctor import Project, build_portable, build_installer, smoke_test, diagnose, collect_licenses
|
|
114
|
+
|
|
115
|
+
project = Project.from_pyproject("pyproject.toml")
|
|
116
|
+
onedir = build_portable(project) # Artifact(kind="onedir", path=..., folder=...)
|
|
117
|
+
result = smoke_test(onedir.path, timeout=10) # SmokeResult(ok, status, missing_modules, ...)
|
|
118
|
+
risk = diagnose(onedir.path, icon_configured=True) # Diagnosis(score, level, factors, defender, ...)
|
|
119
|
+
setup = build_installer(project, onedir.folder) # Windows + Inno Setup
|
|
120
|
+
packages, mode = collect_licenses(project.work_path / "onedir")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`build_all(project, onefile=..., installer=..., smoke_test=..., diagnose=..., defender=..., licenses=..., sbom_file=..., github_tag=...)` runs everything and returns a `Report` with `.ok`, `.steps`, `.to_markdown()` and `.to_json()`.
|
|
124
|
+
|
|
125
|
+
## About the antivirus score
|
|
126
|
+
|
|
127
|
+
The score adds fixed weights for factors that are commonly reported to affect heuristic detection of PyInstaller executables (UPX 30, onefile 20, no version resource 15, unsigned 15, prebuilt bootloader 10, default icon 5). It is meant for comparing your own builds. It does not predict the verdict of Microsoft Defender or any other product, and a low score does not mean a file will not be flagged.
|
|
128
|
+
|
|
129
|
+
pyi-doctor does not obfuscate, encrypt or otherwise hide code from scanners. It applies ordinary distribution practices (version information, no packer, onedir layout, signing advice) and helps you prepare a false-positive report. Code signing with a certificate from a trusted authority is generally the most effective measure and is outside the scope of this tool.
|
|
130
|
+
|
|
131
|
+
## Measured on the bundled example
|
|
132
|
+
|
|
133
|
+
`examples/hello`, Linux x86_64, Python 3.13, PyInstaller 6.22.3 (installer step skipped because it needs Windows):
|
|
134
|
+
|
|
135
|
+
| Step | Time |
|
|
136
|
+
|---|---|
|
|
137
|
+
| onedir build | 4.5 s |
|
|
138
|
+
| onefile build | 12.2 s |
|
|
139
|
+
| licenses + SBOM | 0.8 s |
|
|
140
|
+
| smoke test (both builds) | 0.6 s |
|
|
141
|
+
| diagnosis | < 0.1 s |
|
|
142
|
+
| release folder (zip + checksums) | 1.5 s |
|
|
143
|
+
| **total** | **19.7 s** |
|
|
144
|
+
|
|
145
|
+
Test suite: 138 tests (including 2 real PyInstaller builds) on Python 3.9, 3.11 and 3.13 on Linux. Windows-specific paths (version resource, Inno Setup compilation, Defender scan, error-dialog detection in the smoke test) run in the Windows CI jobs.
|
|
146
|
+
|
|
147
|
+
## Known limitations
|
|
148
|
+
|
|
149
|
+
- Windows features (installer, version resource, Defender scan) need Windows. On other systems the `.iss` script is still written.
|
|
150
|
+
- The smoke test is a heuristic. It removes Python from the environment but does not reproduce a clean PC (installed DLLs, Visual C++ runtime, fonts, etc.). GUI apps that keep running are counted as started. Windowed apps that crash show a PyInstaller error dialog; on Windows pyi-doctor looks for that dialog by window title, which may not catch every case.
|
|
151
|
+
- The bootloader check compares the installed PyInstaller bootloader with the hashes recorded in its wheel. It describes the PyInstaller in the current environment, not an exe built elsewhere.
|
|
152
|
+
- License notices come from package metadata, which can be missing or wrong; packages loaded dynamically may be missed. Review the generated file.
|
|
153
|
+
- Only PyInstaller is supported (no Nuitka / cx_Freeze). No code signing and no auto-update.
|
|
154
|
+
|
|
155
|
+
## Disclaimer
|
|
156
|
+
|
|
157
|
+
pyi-doctor is rule-based and may miss problems or report problems that do not exist. Its reports are informational and do not replace your own testing, review of licenses, or obligations under applicable law; checking the results and complying with license terms and laws remain the user's responsibility. The software is provided "AS IS", without warranty of any kind, under the MIT License.
|
|
158
|
+
|
|
159
|
+
pyi-doctor is an independent project and is not affiliated with or endorsed by the PyInstaller project, Jordan Russell / Inno Setup, Microsoft, or GitHub. Product names are trademarks of their respective owners.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT © 2026 서주원 (OMEGA SYSTEMS) · Contact: seodes105@gmail.com
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyi-doctor"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Build PyInstaller apps as portable exe + installer, then check startup, antivirus risk factors and bundled licenses."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "서주원 (OMEGA SYSTEMS)", email = "seodes105@gmail.com" }]
|
|
14
|
+
keywords = ["pyinstaller", "installer", "inno-setup", "antivirus", "false-positive", "sbom", "windows", "packaging"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Build Tools",
|
|
29
|
+
"Topic :: System :: Software Distribution",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"pyinstaller>=6.0",
|
|
33
|
+
"tomli>=1.1; python_version < '3.11'",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
test = ["pytest>=7"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/sjw0328/pyi-doctor"
|
|
41
|
+
Issues = "https://github.com/sjw0328/pyi-doctor/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
pyi-doctor = "pyi_doctor.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
markers = ["integration: runs a real PyInstaller build (slow)"]
|
|
52
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""pyi-doctor: build PyInstaller apps as a portable exe and an installer, then
|
|
2
|
+
check whether they start outside the build machine, which antivirus risk
|
|
3
|
+
factors they carry, and which third-party licenses they bundle.
|
|
4
|
+
|
|
5
|
+
Typical use::
|
|
6
|
+
|
|
7
|
+
from pyi_doctor import Project, build_all
|
|
8
|
+
|
|
9
|
+
project = Project(entry="main.py", name="MyApp", version="1.0.0", publisher="Me")
|
|
10
|
+
report = build_all(project, log=print)
|
|
11
|
+
print(report.ok)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
|
|
16
|
+
from .config import Project # noqa: E402
|
|
17
|
+
from .errors import BuildError, ConfigError, PyiDoctorError, ToolNotFoundError # noqa: E402
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
"Project",
|
|
22
|
+
"build_all",
|
|
23
|
+
"build_portable",
|
|
24
|
+
"build_installer",
|
|
25
|
+
"diagnose",
|
|
26
|
+
"smoke_test",
|
|
27
|
+
"collect_licenses",
|
|
28
|
+
"PyiDoctorError",
|
|
29
|
+
"ConfigError",
|
|
30
|
+
"BuildError",
|
|
31
|
+
"ToolNotFoundError",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_all(project, **kwargs):
|
|
36
|
+
"""Run the full pipeline. See :func:`pyi_doctor.pipeline.build_all`."""
|
|
37
|
+
from .pipeline import build_all as _impl
|
|
38
|
+
|
|
39
|
+
return _impl(project, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_portable(project, *, onefile: bool = False):
|
|
43
|
+
"""Build the onedir (default) or onefile executable with PyInstaller."""
|
|
44
|
+
from .portable import build
|
|
45
|
+
|
|
46
|
+
return build(project, onefile=onefile)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_installer(project, onedir, **kwargs):
|
|
50
|
+
"""Write and compile the Inno Setup installer for an onedir folder."""
|
|
51
|
+
from .installer import build
|
|
52
|
+
|
|
53
|
+
return build(project, onedir, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def diagnose(exe, **kwargs):
|
|
57
|
+
"""Score an executable for antivirus false-positive risk factors."""
|
|
58
|
+
from .antivirus import diagnose as _impl
|
|
59
|
+
|
|
60
|
+
return _impl(exe, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def smoke_test(exe, **kwargs):
|
|
64
|
+
"""Start an executable in a temp folder with a reduced environment."""
|
|
65
|
+
from .smoke import run
|
|
66
|
+
|
|
67
|
+
return run(exe, **kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def collect_licenses(work_dir=None):
|
|
71
|
+
"""Return (packages, mode) for bundled third-party packages."""
|
|
72
|
+
from .licenses import collect
|
|
73
|
+
|
|
74
|
+
return collect(work_dir)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Small subprocess helpers shared by the build steps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from .errors import BuildError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tail(text: str, lines: int = 25) -> str:
|
|
13
|
+
return "\n".join(text.splitlines()[-lines:])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_logged(
|
|
17
|
+
cmd: Sequence[str],
|
|
18
|
+
*,
|
|
19
|
+
cwd: Path,
|
|
20
|
+
log_path: Path,
|
|
21
|
+
what: str,
|
|
22
|
+
timeout: Optional[float] = None,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Run ``cmd``, write combined output to ``log_path`` and return it.
|
|
25
|
+
|
|
26
|
+
Raises :class:`BuildError` with the last lines of output on failure.
|
|
27
|
+
"""
|
|
28
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
try:
|
|
30
|
+
proc = subprocess.run(
|
|
31
|
+
list(cmd),
|
|
32
|
+
cwd=str(cwd),
|
|
33
|
+
stdout=subprocess.PIPE,
|
|
34
|
+
stderr=subprocess.STDOUT,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
except FileNotFoundError as exc:
|
|
38
|
+
raise BuildError(f"{what}: could not start {cmd[0]!r}: {exc}") from exc
|
|
39
|
+
except subprocess.TimeoutExpired as exc:
|
|
40
|
+
out = (exc.stdout or b"").decode("utf-8", "replace")
|
|
41
|
+
log_path.write_text(out, encoding="utf-8")
|
|
42
|
+
raise BuildError(f"{what} timed out after {timeout}s", tail(out), str(log_path)) from exc
|
|
43
|
+
out = (proc.stdout or b"").decode("utf-8", "replace")
|
|
44
|
+
log_path.write_text(" ".join(_q(c) for c in cmd) + "\n\n" + out, encoding="utf-8")
|
|
45
|
+
if proc.returncode != 0:
|
|
46
|
+
raise BuildError(f"{what} failed with exit code {proc.returncode}", tail(out), str(log_path))
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _q(arg: str) -> str:
|
|
51
|
+
return f'"{arg}"' if (" " in arg or not arg) else arg
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def quote_list(cmd: List[str]) -> str:
|
|
55
|
+
return " ".join(_q(c) for c in cmd)
|