177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
|
|
"""
|
||
|
|
make_msi.py — Create an MSI installer for AUTARCH using Python's built-in msilib.
|
||
|
|
|
||
|
|
Packages the contents of dist/bin/AUTARCH/ (PyInstaller one-dir build) into
|
||
|
|
a Windows Installer .msi file at dist/bin/AUTARCH-{VERSION}-win64.msi.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python scripts/make_msi.py
|
||
|
|
|
||
|
|
Requires:
|
||
|
|
- dist/bin/AUTARCH/ to exist (run PyInstaller first)
|
||
|
|
- Windows (msilib is Windows-only)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import msilib
|
||
|
|
import msilib.schema
|
||
|
|
import msilib.sequence
|
||
|
|
import msilib.text
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import uuid
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
||
|
|
SRC_DIR = Path(__file__).parent.parent
|
||
|
|
BUNDLE_DIR = SRC_DIR / 'dist' / 'bin' / 'AUTARCH'
|
||
|
|
BIN_DIR = SRC_DIR / 'dist' / 'bin'
|
||
|
|
VERSION = '1.3'
|
||
|
|
APP_NAME = 'AUTARCH'
|
||
|
|
MANUFACTURER = 'darkHal Security Group'
|
||
|
|
PRODUCT_CODE = '{6E4A2B35-C8F1-4D28-A91E-8D4F7C3B2A91}'
|
||
|
|
UPGRADE_CODE = '{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}'
|
||
|
|
MSI_OUT = BIN_DIR / f'AUTARCH-{VERSION}-win64.msi'
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def make_id(s: str, prefix: str = '') -> str:
|
||
|
|
"""Create a valid MSI identifier from a string (max 72 chars, no spaces/slashes)."""
|
||
|
|
safe = s.replace('\\', '_').replace('/', '_').replace(' ', '_').replace('.', '_').replace('-', '_')
|
||
|
|
result = (prefix + safe)[:72]
|
||
|
|
if result and result[0].isdigit():
|
||
|
|
result = '_' + result[1:]
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def collect_files(bundle_dir: Path):
|
||
|
|
"""Walk the bundle directory and return (relative_path, abs_path) tuples."""
|
||
|
|
items = []
|
||
|
|
for path in sorted(bundle_dir.rglob('*')):
|
||
|
|
if path.is_file():
|
||
|
|
rel = path.relative_to(bundle_dir)
|
||
|
|
items.append((rel, path))
|
||
|
|
return items
|
||
|
|
|
||
|
|
|
||
|
|
def build_msi():
|
||
|
|
if not BUNDLE_DIR.exists():
|
||
|
|
print(f"ERROR: Bundle directory not found: {BUNDLE_DIR}")
|
||
|
|
print("Run PyInstaller first: pyinstaller autarch.spec --distpath dist/bin")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
BIN_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
print(f"Packaging {BUNDLE_DIR} -> {MSI_OUT}")
|
||
|
|
|
||
|
|
files = collect_files(BUNDLE_DIR)
|
||
|
|
print(f" Files to package: {len(files)}")
|
||
|
|
|
||
|
|
# ── Create the MSI database ───────────────────────────────────────────────
|
||
|
|
db = msilib.init_database(
|
||
|
|
str(MSI_OUT),
|
||
|
|
msilib.schema,
|
||
|
|
APP_NAME,
|
||
|
|
PRODUCT_CODE,
|
||
|
|
VERSION,
|
||
|
|
MANUFACTURER,
|
||
|
|
)
|
||
|
|
msilib.add_tables(db, msilib.sequence)
|
||
|
|
|
||
|
|
# ── Property table (extend — init_database already set some base properties) ──
|
||
|
|
# Use the low-level view to INSERT only new properties
|
||
|
|
try:
|
||
|
|
msilib.add_data(db, 'Property', [
|
||
|
|
('ALLUSERS', '1'),
|
||
|
|
('ARPNOMODIFY', '1'),
|
||
|
|
])
|
||
|
|
except Exception:
|
||
|
|
pass # Properties may already exist from init_database; skip
|
||
|
|
|
||
|
|
# ── Directory structure ───────────────────────────────────────────────────
|
||
|
|
# Collect all unique subdirectories
|
||
|
|
dirs = {}
|
||
|
|
dirs['TARGETDIR'] = ('TARGETDIR', 'SourceDir')
|
||
|
|
dirs['ProgramFilesFolder'] = ('TARGETDIR', 'PFiles')
|
||
|
|
dirs['INSTALLFOLDER'] = ('ProgramFilesFolder', f'{APP_NAME}|{APP_NAME}')
|
||
|
|
|
||
|
|
subdir_set = set()
|
||
|
|
for rel, _ in files:
|
||
|
|
parts = rel.parts[:-1] # directory parts (no filename)
|
||
|
|
for depth in range(len(parts)):
|
||
|
|
sub = '\\'.join(parts[:depth + 1])
|
||
|
|
subdir_set.add(sub)
|
||
|
|
|
||
|
|
# Map subdir path → directory ID
|
||
|
|
dir_id_map = {'': 'INSTALLFOLDER'}
|
||
|
|
dir_rows = [
|
||
|
|
('TARGETDIR', None, 'SourceDir'),
|
||
|
|
('ProgramFilesFolder', 'TARGETDIR', '.'),
|
||
|
|
('INSTALLFOLDER', 'ProgramFilesFolder', APP_NAME),
|
||
|
|
]
|
||
|
|
|
||
|
|
for sub in sorted(subdir_set):
|
||
|
|
parts = sub.split('\\')
|
||
|
|
parent_path = '\\'.join(parts[:-1])
|
||
|
|
parent_id = dir_id_map.get(parent_path, 'INSTALLFOLDER')
|
||
|
|
dir_id = make_id(sub, 'dir_')
|
||
|
|
dir_id_map[sub] = dir_id
|
||
|
|
short_name = parts[-1][:8] # 8.3 name (simplified)
|
||
|
|
long_name = parts[-1]
|
||
|
|
dir_name = f'{short_name}|{long_name}' if short_name != long_name else long_name
|
||
|
|
dir_rows.append((dir_id, parent_id, dir_name))
|
||
|
|
|
||
|
|
msilib.add_data(db, 'Directory', dir_rows)
|
||
|
|
|
||
|
|
# ── Feature ───────────────────────────────────────────────────────────────
|
||
|
|
msilib.add_data(db, 'Feature', [
|
||
|
|
('Main', None, 'AUTARCH Application', 'Complete AUTARCH installation', 1, 1, None, 32),
|
||
|
|
])
|
||
|
|
|
||
|
|
# ── Components and files ──────────────────────────────────────────────────
|
||
|
|
comp_rows = []
|
||
|
|
file_rows = []
|
||
|
|
feat_comp = []
|
||
|
|
|
||
|
|
for idx, (rel, abs_path) in enumerate(files):
|
||
|
|
parts = rel.parts
|
||
|
|
subdir_key = '\\'.join(parts[:-1])
|
||
|
|
dir_id = dir_id_map.get(subdir_key, 'INSTALLFOLDER')
|
||
|
|
comp_id = f'c{idx}'
|
||
|
|
file_id = f'f{idx}'
|
||
|
|
comp_guid = str(uuid.uuid5(uuid.UUID(UPGRADE_CODE), str(rel))).upper()
|
||
|
|
comp_guid = '{' + comp_guid + '}'
|
||
|
|
|
||
|
|
# Component row: (Component, ComponentId, Directory_, Attributes, Condition, KeyPath)
|
||
|
|
comp_rows.append((comp_id, comp_guid, dir_id, 0, None, file_id))
|
||
|
|
|
||
|
|
# File row: (File, Component_, FileName, FileSize, Version, Language, Attributes, Sequence)
|
||
|
|
fname = parts[-1]
|
||
|
|
short = fname[:8]
|
||
|
|
long = fname
|
||
|
|
file_name = f'{short}|{long}' if short != long else long
|
||
|
|
file_size = abs_path.stat().st_size
|
||
|
|
file_rows.append((file_id, comp_id, file_name, file_size, None, None, 512, idx + 1))
|
||
|
|
|
||
|
|
# FeatureComponents: (Feature_, Component_)
|
||
|
|
feat_comp.append(('Main', comp_id))
|
||
|
|
|
||
|
|
msilib.add_data(db, 'Component', comp_rows)
|
||
|
|
msilib.add_data(db, 'File', file_rows)
|
||
|
|
msilib.add_data(db, 'FeatureComponents', feat_comp)
|
||
|
|
|
||
|
|
# ── Media / cabinet ──────────────────────────────────────────────────────
|
||
|
|
# CAB.commit() embeds the cabinet, adds the Media row, and calls db.Commit()
|
||
|
|
cab = msilib.CAB('autarch.cab')
|
||
|
|
for idx, (rel, abs_path) in enumerate(files):
|
||
|
|
# append(full_path, file_id, logical_name_in_cab)
|
||
|
|
cab.append(str(abs_path), f'f{idx}', rel.name)
|
||
|
|
|
||
|
|
cab.commit(db) # handles Media table insert + db.Commit() internally
|
||
|
|
|
||
|
|
size_mb = round(MSI_OUT.stat().st_size / (1024 * 1024), 1)
|
||
|
|
print(f"\n OK: MSI created: {MSI_OUT} ({size_mb} MB)")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
build_msi()
|