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.
Files changed (36) hide show
  1. pyi_doctor-1.0.0/LICENSE +21 -0
  2. pyi_doctor-1.0.0/PKG-INFO +195 -0
  3. pyi_doctor-1.0.0/README.md +163 -0
  4. pyi_doctor-1.0.0/pyproject.toml +52 -0
  5. pyi_doctor-1.0.0/setup.cfg +4 -0
  6. pyi_doctor-1.0.0/src/pyi_doctor/__init__.py +74 -0
  7. pyi_doctor-1.0.0/src/pyi_doctor/__main__.py +5 -0
  8. pyi_doctor-1.0.0/src/pyi_doctor/_proc.py +55 -0
  9. pyi_doctor-1.0.0/src/pyi_doctor/antivirus.py +232 -0
  10. pyi_doctor-1.0.0/src/pyi_doctor/cli.py +183 -0
  11. pyi_doctor-1.0.0/src/pyi_doctor/config.py +281 -0
  12. pyi_doctor-1.0.0/src/pyi_doctor/errors.py +30 -0
  13. pyi_doctor-1.0.0/src/pyi_doctor/installer.py +193 -0
  14. pyi_doctor-1.0.0/src/pyi_doctor/licenses.py +276 -0
  15. pyi_doctor-1.0.0/src/pyi_doctor/pe.py +186 -0
  16. pyi_doctor-1.0.0/src/pyi_doctor/pipeline.py +238 -0
  17. pyi_doctor-1.0.0/src/pyi_doctor/portable.py +121 -0
  18. pyi_doctor-1.0.0/src/pyi_doctor/release.py +104 -0
  19. pyi_doctor-1.0.0/src/pyi_doctor/report.py +154 -0
  20. pyi_doctor-1.0.0/src/pyi_doctor/sbom.py +84 -0
  21. pyi_doctor-1.0.0/src/pyi_doctor/smoke.py +294 -0
  22. pyi_doctor-1.0.0/src/pyi_doctor/versioninfo.py +56 -0
  23. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/PKG-INFO +195 -0
  24. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/SOURCES.txt +34 -0
  25. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/dependency_links.txt +1 -0
  26. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/entry_points.txt +2 -0
  27. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/requires.txt +7 -0
  28. pyi_doctor-1.0.0/src/pyi_doctor.egg-info/top_level.txt +1 -0
  29. pyi_doctor-1.0.0/tests/test_config.py +143 -0
  30. pyi_doctor-1.0.0/tests/test_integration.py +50 -0
  31. pyi_doctor-1.0.0/tests/test_licenses_sbom.py +117 -0
  32. pyi_doctor-1.0.0/tests/test_pe_antivirus.py +142 -0
  33. pyi_doctor-1.0.0/tests/test_pipeline_cli.py +176 -0
  34. pyi_doctor-1.0.0/tests/test_release_report.py +108 -0
  35. pyi_doctor-1.0.0/tests/test_smoke.py +138 -0
  36. pyi_doctor-1.0.0/tests/test_versioninfo_iss.py +156 -0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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)