mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2026-10-25 01:06:05 +03:00
464 lines
14 KiB
Python
464 lines
14 KiB
Python
"""
|
|
build.py - Build the program EXE.
|
|
|
|
Copyright (c) EDCD, All Rights Reserved
|
|
Licensed under the GNU General Public License v2 or later.
|
|
See LICENSE file.
|
|
"""
|
|
|
|
import datetime
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import pathlib
|
|
from string import Template
|
|
import py2exe
|
|
import zipfile
|
|
from config import (
|
|
appcmdname,
|
|
appname,
|
|
appversion,
|
|
copyright,
|
|
git_shorthash_from_head,
|
|
_static_appversion,
|
|
update_interval,
|
|
)
|
|
from update import check_for_fdev_updates, check_for_datafile_updates
|
|
|
|
|
|
AUDIT_DEPS = False
|
|
|
|
if "--audit_deps" in sys.argv:
|
|
AUDIT_DEPS = True
|
|
sys.argv.remove("--audit_deps")
|
|
|
|
DEPENDENCY_DOC_PATH = pathlib.Path("docs") / "Bundled_Python_Dependencies.md"
|
|
|
|
|
|
def iss_build(template_path: str, output_file: str) -> None:
|
|
"""Build the .iss file needed for building the installer EXE."""
|
|
sub_vals = {
|
|
"appver": _static_appversion,
|
|
"update_time": str(update_interval),
|
|
}
|
|
with open(template_path, encoding="UTF8") as template_file:
|
|
src = Template(template_file.read())
|
|
newfile = src.substitute(sub_vals)
|
|
with open(output_file, "w", encoding="UTF8") as new_file:
|
|
new_file.write(newfile)
|
|
|
|
|
|
def system_check(dist_dir: pathlib.Path) -> str:
|
|
"""Check if the system is able to build."""
|
|
if sys.version_info < (3, 13):
|
|
sys.exit(f"EDMC cannot build on Python {sys.version}. Minimum version: 3.13")
|
|
|
|
if sys.platform != "win32":
|
|
sys.exit(f"Unsupported platform {sys.platform}")
|
|
|
|
git_shorthash = git_shorthash_from_head()
|
|
if git_shorthash is None:
|
|
sys.exit("Invalid Git Hash")
|
|
|
|
gitversion_file = ".gitversion"
|
|
with open(gitversion_file, "w+", encoding="utf-8") as gvf:
|
|
gvf.write(git_shorthash)
|
|
|
|
print(f"Git short hash: {git_shorthash}")
|
|
|
|
if dist_dir and pathlib.Path.is_dir(dist_dir):
|
|
shutil.rmtree(dist_dir)
|
|
return gitversion_file
|
|
|
|
|
|
def generate_data_files(
|
|
app_name: str, gitversion_file: str, plugins: list[str]
|
|
) -> list[tuple[object, object]]:
|
|
"""Create the required datafiles to build."""
|
|
l10n_dir = "L10n"
|
|
fdevids_dir = pathlib.Path("FDevIDs")
|
|
license_dir = pathlib.Path("docs/Licenses")
|
|
data_files = [
|
|
(
|
|
"",
|
|
[
|
|
gitversion_file,
|
|
"WinSparkle.dll",
|
|
"WinSparkle.pdb",
|
|
"EUROCAPS.TTF",
|
|
"ChangeLog.md",
|
|
"snd_good.wav",
|
|
"snd_bad.wav",
|
|
f"{app_name}.ico",
|
|
f"resources/{appcmdname}.ico",
|
|
"EDMarketConnector - TRACE.bat",
|
|
"EDMarketConnector - localserver-auth.bat",
|
|
"EDMarketConnector - reset-ui.bat",
|
|
],
|
|
),
|
|
(
|
|
l10n_dir,
|
|
[
|
|
pathlib.Path(l10n_dir) / x
|
|
for x in os.listdir(l10n_dir)
|
|
if x.endswith(".strings")
|
|
],
|
|
),
|
|
(
|
|
fdevids_dir,
|
|
[
|
|
pathlib.Path(fdevids_dir / "commodity.csv"),
|
|
pathlib.Path(fdevids_dir / "rare_commodity.csv"),
|
|
],
|
|
),
|
|
("plugins", plugins),
|
|
]
|
|
# Add all files recursively from license directories
|
|
for root, dirs, files in os.walk(license_dir):
|
|
file_list = [os.path.join(root, f) for f in files]
|
|
dest_dir = os.path.join(license_dir, os.path.relpath(root, license_dir))
|
|
data_files.append((dest_dir, file_list))
|
|
|
|
return data_files
|
|
|
|
|
|
def _scan_dist_for_modules( # noqa: C901, CCR001
|
|
dist_dir: pathlib.Path,
|
|
) -> dict[str, set[str]]:
|
|
"""Return mapping of modules from source path strings."""
|
|
modules: dict[str, set[str]] = {}
|
|
|
|
def add(name: str, origin: str):
|
|
modules.setdefault(name, set()).add(origin)
|
|
|
|
for root, dirs, files in os.walk(dist_dir):
|
|
for name in files:
|
|
path = pathlib.Path(root) / name
|
|
|
|
# Loose files
|
|
if path.suffix in (".py", ".pyc"):
|
|
try:
|
|
rel = path.relative_to(dist_dir)
|
|
except ValueError:
|
|
continue
|
|
|
|
parts = rel.parts
|
|
if not parts:
|
|
continue
|
|
|
|
top = parts[0]
|
|
if top.endswith((".py", ".pyc")):
|
|
top = pathlib.Path(top).stem
|
|
|
|
add(top, str(rel))
|
|
|
|
# Zips
|
|
if path.suffix == ".zip":
|
|
try:
|
|
with zipfile.ZipFile(path) as z:
|
|
for info in z.infolist():
|
|
if info.filename.endswith((".py", ".pyc")):
|
|
parts = info.filename.split("/") # type: ignore
|
|
if not parts:
|
|
continue
|
|
|
|
top = parts[0]
|
|
if top.endswith((".py", ".pyc")):
|
|
top = pathlib.Path(top).stem
|
|
|
|
add(top, f"{path.name}:{info.filename}")
|
|
except zipfile.BadZipFile:
|
|
pass
|
|
|
|
modules.pop("__pycache__", None)
|
|
modules.pop("", None)
|
|
|
|
return modules
|
|
|
|
|
|
def _is_project_module(module_name: str, project_root: pathlib.Path) -> bool:
|
|
"""Determine if this module name corresponds to EDMC source code."""
|
|
if (project_root / f"{module_name}.py").exists():
|
|
return True
|
|
if (project_root / module_name).is_dir():
|
|
return True
|
|
return False
|
|
|
|
|
|
def _discover_project_modules(project_root: pathlib.Path) -> set[str]:
|
|
"""Discover top-level Python modules/packages in the project source tree."""
|
|
mods: set[str] = set()
|
|
|
|
for entry in project_root.iterdir():
|
|
if entry.name.startswith("."):
|
|
continue
|
|
|
|
if entry.is_file() and entry.suffix == ".py":
|
|
mods.add(entry.stem)
|
|
|
|
elif entry.is_dir():
|
|
if (entry / "__init__.py").exists():
|
|
mods.add(entry.name)
|
|
|
|
return mods
|
|
|
|
|
|
def _classify_modules(
|
|
modules: set[str], project_modules: set[str]
|
|
) -> tuple[list[str], list[str], list[str]]:
|
|
"""Split modules into stdlib, third-party, and custom."""
|
|
stdlib = set(sys.stdlib_module_names)
|
|
|
|
stdlib_mods: list[str] = []
|
|
third_party_mods: list[str] = []
|
|
custom_mods: list[str] = []
|
|
|
|
for name in sorted(modules):
|
|
if name in stdlib:
|
|
stdlib_mods.append(name)
|
|
elif name in project_modules:
|
|
custom_mods.append(name)
|
|
else:
|
|
third_party_mods.append(name)
|
|
|
|
return stdlib_mods, third_party_mods, custom_mods
|
|
|
|
|
|
def write_dependency_markdown(dist_dir: pathlib.Path) -> None: # noqa: CCR001
|
|
"""Generate docs/Bundled_Python_Dependencies.md.
|
|
|
|
Lists:
|
|
- Third-party modules bundled
|
|
- Standard library modules that are actually bundled
|
|
Excludes:
|
|
- All EDMC project code
|
|
"""
|
|
module_map = _scan_dist_for_modules(dist_dir)
|
|
project_root = pathlib.Path(__file__).parent
|
|
stdlib = set(sys.stdlib_module_names)
|
|
|
|
bundled_stdlib: list[str] = []
|
|
bundled_third_party: list[str] = []
|
|
|
|
for name in sorted(module_map):
|
|
# Exclude EDMC code by filesystem presence
|
|
if _is_project_module(name, project_root):
|
|
continue
|
|
|
|
# Exclude entry point and Python internal test modules
|
|
if name in ("__main__",) or name.startswith("_test"):
|
|
continue
|
|
|
|
if name in stdlib:
|
|
bundled_stdlib.append(name)
|
|
else:
|
|
bundled_third_party.append(name)
|
|
|
|
DEPENDENCY_DOC_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(DEPENDENCY_DOC_PATH, "w", encoding="utf-8") as f:
|
|
f.write("# Bundled Python Dependencies\n\n")
|
|
f.write("This file is **auto-generated by the build system**.\n\n")
|
|
f.write(
|
|
f"This file was last updated for {_static_appversion} on {datetime.date.today()}.\n\n"
|
|
)
|
|
|
|
f.write(
|
|
"This document lists Python modules that are bundled into the EDMC"
|
|
" executable by the Windows build process.\n"
|
|
)
|
|
f.write(
|
|
"It is intended for **transparency, auditing, debugging, and license compliance**.\n\n"
|
|
)
|
|
|
|
f.write("## Important Notes\n\n")
|
|
f.write("- **This is NOT a public API guarantee.**\n")
|
|
f.write(
|
|
"- The presence of a module in this list does **NOT** mean it is "
|
|
"supported for plugins or external use.\n"
|
|
)
|
|
f.write(
|
|
"- Bundled modules may be **added, removed, upgraded, downgraded, or "
|
|
"replaced at any time** without notice.\n"
|
|
)
|
|
f.write(
|
|
"- This list reflects **one specific build configuration** and may differ "
|
|
"between releases, platforms, or build options.\n\n"
|
|
)
|
|
|
|
f.write("## For Plugin Developers\n\n")
|
|
f.write("- Always vendor or declare your own dependencies.\n")
|
|
f.write("- Only the documented EDMC plugin API is considered stable.\n")
|
|
f.write("- Use try/except imports for optional features.\n")
|
|
f.write("- Do not rely on pywin32 internals for cross-platform plugins.\n")
|
|
f.write(
|
|
"- While it is likely that these dependencies will be available, this is not part of the public API.\n\n"
|
|
)
|
|
|
|
f.write("## For Developers and Packagers\n\n")
|
|
f.write("- This reflects what py2exe determined was required for this build.\n")
|
|
f.write(
|
|
"- Changes in Python, py2exe, or import structure may significantly change this list.\n"
|
|
)
|
|
f.write(
|
|
"- Only modules actually bundled by py2exe are listed; this is "
|
|
"**not all of the Python standard library**.\n"
|
|
)
|
|
f.write(
|
|
"- This list is **not sorted by importance, stability, or API relevance**.\n\n"
|
|
)
|
|
f.write(
|
|
"EDMC's own source code and first-party modules are intentionally excluded from this document.\n\n"
|
|
)
|
|
|
|
f.write("## Bundled Standard Library Modules\n\n")
|
|
if not bundled_stdlib:
|
|
f.write("*(None)*\n")
|
|
else:
|
|
for name in bundled_stdlib:
|
|
f.write(f"- `{name}`\n")
|
|
|
|
f.write("\n## Bundled Third-Party Modules\n\n")
|
|
if not bundled_third_party:
|
|
f.write("*(None)*\n")
|
|
else:
|
|
for name in bundled_third_party:
|
|
note = ""
|
|
if name.startswith("win32") or name in (
|
|
"pythoncom",
|
|
"pywintypes",
|
|
"pywin32_system32",
|
|
):
|
|
note = " (Windows-only, pywin32)"
|
|
f.write(f"- `{name}`{note}\n")
|
|
|
|
f.write("\n---\n")
|
|
f.write(f"\nTotal bundled stdlib modules: {len(bundled_stdlib)}\n")
|
|
f.write(f"Total bundled third-party modules: {len(bundled_third_party)}\n")
|
|
|
|
print(f"Wrote dependency manifest to {DEPENDENCY_DOC_PATH}")
|
|
|
|
|
|
def build(audit_deps: bool = False) -> None:
|
|
"""Build EDMarketConnector using Py2Exe."""
|
|
dist_dir: pathlib.Path = pathlib.Path("dist.win32")
|
|
gitversion_filename: str = system_check(dist_dir)
|
|
|
|
# Constants
|
|
plugins: list[str] = [
|
|
"plugins/coriolis.py",
|
|
"plugins/eddn.py",
|
|
"plugins/edsm.py",
|
|
"plugins/edsy.py",
|
|
"plugins/inara.py",
|
|
"plugins/spansh_core.py",
|
|
"plugins/edastro_core.py",
|
|
"plugins/common_coreutils.py",
|
|
]
|
|
options: dict = {
|
|
"py2exe": {
|
|
"dist_dir": dist_dir,
|
|
"optimize": 2,
|
|
"packages": ["asyncio", "multiprocessing", "sqlite3", "util", "plugins"],
|
|
"includes": ["dataclasses", "shutil", "timeout_session", "zipfile"],
|
|
"excludes": [
|
|
"distutils",
|
|
"_markerlib",
|
|
"optparse",
|
|
"simplejson",
|
|
"unittest",
|
|
"doctest",
|
|
"pdb",
|
|
"difflib",
|
|
],
|
|
}
|
|
}
|
|
|
|
data_files = generate_data_files(appname, gitversion_filename, plugins)
|
|
|
|
version_info: dict = {
|
|
"description": "Elite Dangerous Market Connector (EDMC)",
|
|
"comments": "Downloads commodity market and other station data from the game"
|
|
" Elite Dangerous for use with all popular online and offline trading tools.",
|
|
"company_name": "EDCD",
|
|
"product_name": appname,
|
|
"version": str(appversion().truncate()),
|
|
"product_version": str(appversion()),
|
|
"copyright": copyright,
|
|
"language": "English (United States)",
|
|
}
|
|
|
|
windows_config: dict = {
|
|
"dest_base": appname,
|
|
"script": "EDMarketConnector.py",
|
|
"icon_resources": [(0, f"{appname}.ico")],
|
|
"other_resources": [
|
|
(
|
|
24,
|
|
1,
|
|
pathlib.Path(f"resources/{appname}.manifest").read_text(
|
|
encoding="UTF8"
|
|
),
|
|
)
|
|
],
|
|
}
|
|
|
|
console_config: dict = {
|
|
"dest_base": appcmdname,
|
|
"script": "EDMC.py",
|
|
"icon_resources": [(0, f"resources/{appcmdname}.ico")],
|
|
"other_resources": [
|
|
(
|
|
24,
|
|
1,
|
|
pathlib.Path(f"resources/{appcmdname}.manifest").read_text(
|
|
encoding="UTF8"
|
|
),
|
|
)
|
|
],
|
|
}
|
|
|
|
checker_config: dict = {
|
|
"dest_base": "EDMCSystemProfiler",
|
|
"script": "EDMCSystemProfiler.py",
|
|
"icon_resources": [(0, f"{appname}.ico")],
|
|
"other_resources": [
|
|
(
|
|
24,
|
|
1,
|
|
pathlib.Path(f"resources/{appname}.manifest").read_text(
|
|
encoding="UTF8"
|
|
),
|
|
)
|
|
],
|
|
}
|
|
|
|
try:
|
|
py2exe.freeze(
|
|
version_info=version_info,
|
|
windows=[windows_config, checker_config],
|
|
console=[console_config],
|
|
data_files=data_files,
|
|
options=options,
|
|
)
|
|
except FileNotFoundError as err:
|
|
print(err)
|
|
sys.exit(
|
|
"Build Failed due to Missing Files! Have you set up your submodules? \n"
|
|
"https://github.com/EDCD/EDMarketConnector/wiki/Running-from-source"
|
|
"#obtain-a-copy-of-the-application-source"
|
|
)
|
|
|
|
if audit_deps:
|
|
write_dependency_markdown(dist_dir)
|
|
|
|
iss_template_path: str = "./resources/EDMC_Installer_Config_template.txt"
|
|
iss_file_path: str = "./EDMC_Installer_Config.iss"
|
|
iss_build(iss_template_path, iss_file_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
check_for_fdev_updates(local=True)
|
|
check_for_datafile_updates(local=True)
|
|
build(audit_deps=AUDIT_DEPS)
|