1 parent
24b624301c
commit
6213fec7d2
29 files changed
+4058
-5
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"""Prepare ISM-derived inputs for the existing MSIX Studio GUI/CLI.
|
||||
|
||||
This script does not run MakeAppx, install software, or modify source files.
|
||||
Packaging and verification must be performed by package-builder.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import re
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def prepare_payload(out, mappings):
|
||||
"""Copy only resolved ISM files; never hard-link or edit the build outputs."""
|
||||
payload = out / "payload"
|
||||
payload.mkdir()
|
||||
for index, item in enumerate(mappings, 1):
|
||||
source = Path(item["source"])
|
||||
target = payload / item["target"]
|
||||
if not target.resolve().is_relative_to(payload.resolve()):
|
||||
raise ValueError("Payload target escaped workspace")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
before = source.stat()
|
||||
digest = hashlib.sha256()
|
||||
with source.open("rb") as src, target.open("xb") as dst:
|
||||
while chunk := src.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
dst.write(chunk)
|
||||
after = source.stat()
|
||||
if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
|
||||
raise ValueError(f"Source changed while copying: {source}")
|
||||
with target.open("rb") as check:
|
||||
if hashlib.file_digest(check, "sha256").hexdigest() != digest.hexdigest():
|
||||
raise ValueError(f"Copy mismatch: {source}")
|
||||
item["preparedSource"] = str(target)
|
||||
item["sha256"] = digest.hexdigest()
|
||||
if index % 500 == 0:
|
||||
print(f"Prepared {index}/{len(mappings)}", flush=True)
|
||||
return payload
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repo", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--makeappx", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
repo, out = args.repo.resolve(), args.output.resolve()
|
||||
if out.exists():
|
||||
raise ValueError(f"Output already exists: {out}")
|
||||
if not args.makeappx.is_file():
|
||||
raise ValueError("MakeAppx not found")
|
||||
ism = repo / "InstallShield/src/EG-BIM_V1.13.1.ism"
|
||||
doc = ET.parse(ism).getroot()
|
||||
|
||||
def table(name):
|
||||
t = doc.find(f"table[@name='{name}']")
|
||||
if t is None:
|
||||
return []
|
||||
cols = [c.text for c in t.findall("col")]
|
||||
return [dict(zip(cols, [c.text or "" for c in row])) for row in t.findall("row")]
|
||||
|
||||
strings = {x["ISString"]: x["Value"] for x in table("ISString") if x["ISLanguage_"] == "1042"}
|
||||
props = {x["Property"]: re.sub(r"##(.*?)##", lambda m: strings.get(m[1], m[0]), x["Value"])
|
||||
for x in table("Property")}
|
||||
build = repo / "Build/EG-BIM/EG-BIM.Pro.64bit"
|
||||
viewer = repo / "CoWork/Import/EgViewer"
|
||||
engine = repo / "Source/Engine/IntelliCAD/Source"
|
||||
bindings = {
|
||||
"PATH_TO_GRM_FILES": build,
|
||||
"PATH_TO_EGVIEWER_FILES": viewer,
|
||||
"PATH_TO_RESOURCES_FILES1": viewer / "Resources",
|
||||
"PATH_TO_ICON_FILES": engine / "Resources/Common/brand",
|
||||
"PATH_TO_SPELLING_FILES": engine / "Resources/Localized/ENU/Spelling",
|
||||
"PATH_TO_TABLET_FILES": engine / "IntelliCAD/Installer/3rdParty/Tablet",
|
||||
"PATH_TO_TEXTURES_FILES": engine / "IntelliCAD/IRX/RenderEngine/Textures",
|
||||
}
|
||||
dirs = {x["Directory"]: x for x in table("Directory")}
|
||||
comps = {x["Component"]: x for x in table("Component")}
|
||||
|
||||
def directory(key, stack=()):
|
||||
if key in stack:
|
||||
raise ValueError("Directory cycle")
|
||||
if key == "ProgramFiles64Folder":
|
||||
return "VFS/ProgramFilesX64"
|
||||
if key == "AppDataFolder":
|
||||
# MSIX Studio currently has no first-launch AppData migration handler.
|
||||
# Preserve these inputs explicitly as seeds, NOT as a fictitious VFS mapping.
|
||||
return "UserDataSeeds/Roaming"
|
||||
d = dirs[key]
|
||||
part = d["DefaultDir"].split(":")[0].split("|")[-1]
|
||||
return directory(d["Directory_Parent"], (*stack, key)) + ("/" + part if part != "." else "")
|
||||
|
||||
groups, mappings, seen = collections.OrderedDict(), [], set()
|
||||
source_counts = collections.Counter()
|
||||
for row in table("File"):
|
||||
match = re.fullmatch(r"<([^>]+)>\\?(.*)", row["ISBuildSourcePath"])
|
||||
if not match or match[1] not in bindings:
|
||||
raise ValueError(f"Unsupported ISM source: {row['File']}")
|
||||
source_root = bindings[match[1]]
|
||||
source = source_root / match[2]
|
||||
reason = "path-variable-remap"
|
||||
if not source.is_file() and match[1] == "PATH_TO_GRM_FILES" and match[2].lower().startswith("icon\\"):
|
||||
source_root = bindings["PATH_TO_ICON_FILES"]
|
||||
source = source_root / PureWindowsPath(match[2]).name
|
||||
reason = "brand-icon-resource-fallback"
|
||||
if not source.is_file() and match[2].lower() == "fonts\\kosdic.shx":
|
||||
source_root = repo / "Source/Resources/CadFonts"
|
||||
source = source_root / "KOSDIC.SHX"
|
||||
reason = "cad-font-resource-fallback"
|
||||
if not source.is_file():
|
||||
raise FileNotFoundError(f"Unresolved ISM file {row['File']}: {source}")
|
||||
source = source.resolve()
|
||||
if not source.is_relative_to(repo):
|
||||
raise ValueError("Source escaped repository")
|
||||
for ancestor in (source, *source.parents):
|
||||
if ancestor.is_symlink() or (hasattr(ancestor, "is_junction") and ancestor.is_junction()):
|
||||
raise ValueError(f"Linked source: {ancestor}")
|
||||
target = directory(comps[row["Component_"]]["Directory_"]) + "/" + row["FileName"].split("|")[-1]
|
||||
relative = source.relative_to(source_root).as_posix()
|
||||
if not target.lower().endswith("/" + relative.lower()):
|
||||
# Preserve MSI destination instead of flattening files or guessing by basename.
|
||||
source_root = source.parent
|
||||
relative = source.name
|
||||
if not target.lower().endswith("/" + relative.lower()):
|
||||
raise ValueError(f"File rename requires an explicit adapter: {row['File']}")
|
||||
prefix = target[:-(len(relative) + 1)]
|
||||
actual_target = prefix + "/" + relative
|
||||
if actual_target.lower() in seen:
|
||||
raise ValueError(f"Duplicate target: {actual_target}")
|
||||
if any(ch in relative for ch in "[]{}*?"):
|
||||
raise ValueError(f"Literal glob escaping required: {relative}")
|
||||
seen.add(actual_target.lower())
|
||||
groups.setdefault((str(source_root), prefix), []).append(relative)
|
||||
source_counts[match[1]] += 1
|
||||
mappings.append({"ismFileId": row["File"], "ismSource": row["ISBuildSourcePath"],
|
||||
"source": str(source), "target": actual_target,
|
||||
"ismDirectoryId": comps[row["Component_"]]["Directory_"],
|
||||
"bytes": source.stat().st_size, "resolution": reason,
|
||||
"placement": "appdata-seed-only" if target.startswith("UserDataSeeds/") else "package-vfs"})
|
||||
|
||||
out.mkdir(parents=True)
|
||||
(out / "reports").mkdir()
|
||||
(out / "assets").mkdir()
|
||||
subprocess.run(["powershell.exe", "-NoProfile", "-File",
|
||||
str(Path(__file__).with_name("New-EgbimSampleAssets.ps1")),
|
||||
"-IconPath", str(engine / "Resources/Common/brand/Grimi.ico"),
|
||||
"-OutputFolder", str(out / "assets")], check=True)
|
||||
payload = prepare_payload(out, mappings)
|
||||
# A single directory rule avoids compiling thousands of literal glob patterns.
|
||||
sources = [{"alias": "ism-payload"}]
|
||||
rules = [{"id": "ism-payload", "source": "ism-payload", "include": ["**/*"],
|
||||
"exclude": [], "targetRelativePath": "", "required": True}]
|
||||
local_sources = {"ism-payload": str(payload)}
|
||||
sources.append({"alias": "sample-assets"})
|
||||
local_sources["sample-assets"] = str(out / "assets")
|
||||
rules.append({"id": "sample-assets", "source": "sample-assets", "include": ["Logo44.png", "Logo150.png"],
|
||||
"exclude": [], "targetRelativePath": "Assets", "required": True})
|
||||
project = {
|
||||
"schemaVersion": 2, "projectId": "egbim-ism-msix-sample",
|
||||
"product": {"displayName": "EG-BIM MSIX Sample", "manufacturer": props["Manufacturer"],
|
||||
"displayVersion": props["ProductVersion"]},
|
||||
"sources": sources, "files": rules,
|
||||
"formats": {"msix": {
|
||||
"identity": {"name": "Baron.EGBIM.Sample", "publisher": "CN=Baron MSIX Sample",
|
||||
"version": props["ProductVersion"], "architecture": "x64"},
|
||||
"minimumOsVersion": "10.0.19041.0",
|
||||
"applications": [{"id": "EGBIM", "executable": "VFS/ProgramFilesX64/Baron/EG-BIM/Grimi.exe",
|
||||
"entryPoint": "Windows.FullTrustApplication", "square44x44Logo": "Assets/Logo44.png",
|
||||
"square150x150Logo": "Assets/Logo150.png"}],
|
||||
"capabilities": ["runFullTrust"], "extensions": [], "dependencies": []}},
|
||||
"targets": [{"id": "msix-direct", "format": "msix", "delivery": "package", "distribution": "direct",
|
||||
"signingProfileRef": None}]}
|
||||
write_json(out / "package-project.json", project)
|
||||
write_json(out / "package-project.local.json", {"sources": local_sources, "makeAppx": str(args.makeappx.resolve())})
|
||||
write_json(out / "reports/file-mapping.json", mappings)
|
||||
with (out / "reports/file-mapping.csv").open("w", encoding="utf-8-sig", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=list(mappings[0])); writer.writeheader(); writer.writerows(mappings)
|
||||
write_json(out / "reports/registry-not-converted.json", {"applied": False, "rows": table("Registry"), "properties": props})
|
||||
write_json(out / "reports/legacy-actions-not-executed.json", {
|
||||
"customActions": table("CustomAction"), "prerequisites": table("ISSetupPrerequisites"),
|
||||
"features": table("Feature"), "featureComponents": table("FeatureComponents"),
|
||||
"conditions": table("Condition"), "shortcuts": table("Shortcut")})
|
||||
summary = {"ism": str(ism), "ismSha256": hashlib.file_digest(ism.open("rb"), "sha256").hexdigest(),
|
||||
"buildRoot": str(build), "viewerRoot": str(viewer), "ismFileCount": len(mappings),
|
||||
"resolvedFileCount": len(mappings), "missingFileCount": 0,
|
||||
"sourceBytes": sum(x["bytes"] for x in mappings), "ruleCount": len(rules),
|
||||
"appDataSeedFiles": sum(x["placement"] == "appdata-seed-only" for x in mappings),
|
||||
"sourceCounts": dict(source_counts), "registryRowsNotApplied": len(table("Registry")),
|
||||
"signing": "unsignedDevelopment", "installationTest": "notRun", "deploymentReady": False,
|
||||
"limitations": ["No registry.dat / packaged COM / file associations generated by current builder",
|
||||
"EgViewer AppData files are preserved as UserDataSeeds; no migration/activation is implemented",
|
||||
"InstallShield CustomActions and prerequisite installers are not run",
|
||||
"Runtime dependencies and first launch require a separate VM compatibility test",
|
||||
"Sample identity/publisher are not production signing or Store identity"]}
|
||||
write_json(out / "reports/preparation.json", summary)
|
||||
print(json.dumps(summary, ensure_ascii=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user