Skip to content

Commit 0914f75

Browse files
committed
Optimize MSI installer with archive-based extraction
Changes from 49K-file MSI to zip-extract approach: bundle 7za.exe and zip archive, extract during install with VBScript for silent operation, no compression on pre-compressed content, fast cleanup on uninstall.
1 parent bce1a76 commit 0914f75

File tree

6 files changed

+97
-99
lines changed

6 files changed

+97
-99
lines changed

scripts/build_installer.bat

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ popd
2929
@REM Build installer
3030
@REM ===========================================================================
3131
call %FUNC% DeployPython
32+
33+
@REM Create archive of Python environment
34+
@REM ===========================================================================
35+
echo Creating archive of Python environment...
36+
set ARCHIVE_PATH=%ROOTPATH%\dist\%CI_DST%-files.zip
37+
if exist "%ARCHIVE_PATH%" ( del /q "%ARCHIVE_PATH%" )
38+
pushd %ROOTPATH%\dist\%CI_DST%
39+
"C:\Program Files\7-Zip\7z.exe" a -tzip -mx=1 "%ARCHIVE_PATH%" *
40+
popd
41+
echo Archive created: %ARCHIVE_PATH%
42+
3243
echo Generating .wxs file for WiX installer...
3344
%PYTHON% "wix\makewxs.py" %CI_DST% %CI_VER%
3445
echo Building WiX Installer...

wix/7za.exe

574 KB
Binary file not shown.

wix/cleanup.vbs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Set objShell = CreateObject("WScript.Shell")
2+
Set objArgs = WScript.Arguments
3+
If objArgs.Count >= 1 Then
4+
targetFolder = objArgs(0)
5+
objShell.Run "cmd.exe /c rd /s /q """ & targetFolder & """", 0, True
6+
End If

wix/extract.vbs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Set objShell = CreateObject("WScript.Shell")
2+
Set objArgs = WScript.Arguments
3+
If objArgs.Count >= 2 Then
4+
sevenZipPath = objArgs(0)
5+
zipPath = objArgs(1)
6+
targetPath = objArgs(2)
7+
objShell.Run """" & sevenZipPath & """ x -y """ & zipPath & """ -o""" & targetPath & """", 0, True
8+
End If

wix/generic-DataLab-WinPython.wxs

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,75 @@
11
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
22
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
3-
<Package Name="DataLab-WinPython" ProductCode="f3313a2e-1d05-4a9f-af25-046ac0227a95" Language="1033" Version="{version}" Codepage="1252" Manufacturer="DataLab Platform Developers" UpgradeCode="a8002fab-c887-4ada-9792-ba1aedae50b6" InstallerVersion="200" Scope="perUserOrMachine">
3+
<Package Name="DataLab-WinPython" ProductCode="f3313a2e-1d05-4a9f-af25-046ac0227a95" Language="1033" Version="{version}" Codepage="1252" Manufacturer="DataLab Platform Developers" UpgradeCode="a8002fab-c887-4ada-9792-ba1aedae50b6" InstallerVersion="500" Scope="perUserOrMachine" Compressed="yes">
44
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
55
<Icon Id="DataLab.exe" SourceFile=".\executables\DataLab.ico" />
66
<Icon Id="DataLabResetIcon" SourceFile=".\executables\DataLab-Reset.ico" />
77
<WixVariable Id="WixUILicenseRtf" Value=".\wix\license.rtf" />
88
<WixVariable Id="WixUIDialogBmp" Value=".\wix\dialog.bmp" />
99
<WixVariable Id="WixUIBannerBmp" Value=".\wix\banner.bmp" />
10-
<MediaTemplate EmbedCab="yes" />
10+
<MediaTemplate EmbedCab="yes" CompressionLevel="none" />
11+
<Property Id="MSIFASTINSTALL" Value="2" />
1112
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER"/>
1213
<Feature Id="ProductFeature" Title="DataLab-WinPython" Level="1">
1314
<ComponentGroupRef Id="ProductComponents" />
1415
</Feature>
15-
<Property Id="REMOVEPROP">
16-
<RegistrySearch Id="GetInstallFolderForNukingPurposes" Root="HKCU" Key="SOFTWARE\[Manufacturer]\[ProductName]" Name="InstallFolder" Type="directory" />
17-
</Property>
16+
17+
<!-- Custom actions for archive extraction using bundled 7za.exe with hidden VBScript wrappers -->
18+
<CustomAction Id="ExtractArchive" Directory="INSTALLFOLDER" Execute="deferred" Impersonate="no" HideTarget="yes"
19+
ExeCommand="&quot;[System64Folder]wscript.exe&quot; &quot;[INSTALLFOLDER]extract.vbs&quot; &quot;[INSTALLFOLDER]7za.exe&quot; &quot;[INSTALLFOLDER]DataLab-WinPython-files.zip&quot; &quot;[INSTALLFOLDER]&quot;"
20+
Return="check" />
21+
22+
<CustomAction Id="DeleteArchive" Directory="INSTALLFOLDER" Execute="deferred" Impersonate="no" HideTarget="yes"
23+
ExeCommand="&quot;[System64Folder]cmd.exe&quot; /c &quot;del /q &quot;[INSTALLFOLDER]DataLab-WinPython-files.zip&quot; &amp; del /q &quot;[INSTALLFOLDER]7za.exe&quot; &amp; del /q &quot;[INSTALLFOLDER]extract.vbs&quot;&quot;"
24+
Return="ignore" />
25+
26+
<CustomAction Id="CleanupOnUninstall" Directory="INSTALLFOLDER" Execute="deferred" Impersonate="no" HideTarget="yes"
27+
ExeCommand="&quot;[System64Folder]wscript.exe&quot; &quot;[INSTALLFOLDER]cleanup.vbs&quot; &quot;[INSTALLFOLDER]&quot;"
28+
Return="ignore" />
29+
30+
<InstallExecuteSequence>
31+
<Custom Action="ExtractArchive" After="InstallFiles" Condition="NOT Installed" />
32+
<Custom Action="DeleteArchive" After="ExtractArchive" Condition="NOT Installed" />
33+
<Custom Action="CleanupOnUninstall" Before="RemoveFiles" Condition="REMOVE=&quot;ALL&quot;" />
34+
</InstallExecuteSequence>
1835
</Package>
1936
<Fragment>
2037
<StandardDirectory Id="ProgramFilesFolder">
21-
<Directory Id="INSTALLFOLDER" Name="DataLab-WinPython">
22-
<!-- Automatically inserted directories -->
23-
</Directory>
38+
<Directory Id="INSTALLFOLDER" Name="DataLab-WinPython" />
2439
</StandardDirectory>
2540
<StandardDirectory Id="ProgramMenuFolder">
2641
<Directory Id="ApplicationProgramsFolder" Name="DataLab-WinPython" />
2742
</StandardDirectory>
2843
</Fragment>
2944
<Fragment>
3045
<ComponentGroup Id="ProductComponents">
31-
<Component Id="PC_Files" Directory="INSTALLFOLDER" Guid="502b999f-a88e-464b-ba6c-a8c987937c41">
46+
<!-- 7za.exe standalone extractor -->
47+
<Component Id="PC_7za" Directory="INSTALLFOLDER" Guid="b2c3d4e5-f6a7-5b6c-9d0e-1f2a3b4c5d6e">
48+
<File Id="File7za" Name="7za.exe" Source=".\wix\7za.exe" KeyPath="yes" />
49+
</Component>
50+
51+
<!-- VBScript helpers for silent extraction -->
52+
<Component Id="PC_VBScripts" Directory="INSTALLFOLDER" Guid="c3d4e5f6-a7b8-6c7d-0e1f-2a3b4c5d6e7f">
53+
<File Id="FileExtractVBS" Name="extract.vbs" Source=".\wix\extract.vbs" KeyPath="yes" />
54+
<File Id="FileCleanupVBS" Name="cleanup.vbs" Source=".\wix\cleanup.vbs" />
55+
</Component>
56+
57+
<!-- Archive component -->
58+
<Component Id="PC_Archive" Directory="INSTALLFOLDER" Guid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d">
59+
<File Id="ArchiveFile" Name="DataLab-WinPython-files.zip" Source="{archive_path}" KeyPath="yes" />
60+
</Component>
61+
62+
<!-- Launcher executables component -->
63+
<Component Id="PC_Executables" Directory="INSTALLFOLDER" Guid="502b999f-a88e-464b-ba6c-a8c987937c41">
3264
<File Source=".\dist\DataLab-WinPython\DataLab.exe" KeyPath="yes" />
33-
<File Source=".\dist\DataLab-WinPython\Jupyter Lab.exe" KeyPath="no" />
34-
<File Source=".\dist\DataLab-WinPython\Jupyter Notebook.exe" KeyPath="no" />
35-
<File Source=".\dist\DataLab-WinPython\Spyder.exe" KeyPath="no" />
36-
<File Source=".\dist\DataLab-WinPython\Spyder reset.exe" KeyPath="no" />
37-
<File Source=".\dist\DataLab-WinPython\WinPython Command Prompt.exe" KeyPath="no" />
38-
<File Source=".\dist\DataLab-WinPython\WinPython Control Panel.exe" KeyPath="no" />
39-
<File Source=".\dist\DataLab-WinPython\WinPython Interpreter.exe" KeyPath="no" />
65+
<File Source=".\dist\DataLab-WinPython\Jupyter Lab.exe" />
66+
<File Source=".\dist\DataLab-WinPython\Jupyter Notebook.exe" />
67+
<File Source=".\dist\DataLab-WinPython\Spyder.exe" />
68+
<File Source=".\dist\DataLab-WinPython\Spyder reset.exe" />
69+
<File Source=".\dist\DataLab-WinPython\WinPython Command Prompt.exe" />
70+
<File Source=".\dist\DataLab-WinPython\WinPython Control Panel.exe" />
71+
<File Source=".\dist\DataLab-WinPython\WinPython Interpreter.exe" />
4072
</Component>
41-
<!-- Automatically inserted components -->
4273
<!-- Shortcut components: one component per shortcut with registry KeyPath -->
4374
<Component Id="PC_Shortcut_Main" Directory="ApplicationProgramsFolder" Guid="*">
4475
<Shortcut Id="ApplicationStartMenuShortcut" Name="DataLab" Description="DataLab" Target="[INSTALLFOLDER]\DataLab.exe" WorkingDirectory="INSTALLFOLDER" Icon="DataLab.exe" />
@@ -92,8 +123,6 @@
92123
</Component>
93124
<Component Id="PC_Registry" Directory="INSTALLFOLDER" Guid="*">
94125
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Name="installed" Type="integer" Value="1" KeyPath="yes" />
95-
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Name="InstallFolder" Type="string" Value="[INSTALLFOLDER]" />
96-
<util:RemoveFolderEx On="both" Property="REMOVEPROP" />
97126
</Component>
98127
</ComponentGroup>
99128
</Fragment>

wix/makewxs.py

Lines changed: 24 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import argparse
1717
import os
1818
import os.path as osp
19-
import uuid
20-
import xml.etree.ElementTree as ET
2119

2220
COUNT = 0
2321

@@ -48,93 +46,39 @@ def insert_text_after(text: str, containing: str, content: str) -> str:
4846
def make_wxs(product_name: str, version: str) -> None:
4947
"""Make a .wxs file for the DataLab Windows installer."""
5048
wix_dir = osp.abspath(osp.dirname(__file__))
51-
proj_dir = osp.join(wix_dir, os.pardir)
49+
proj_dir = osp.abspath(osp.join(wix_dir, os.pardir))
5250
dist_dir = osp.join(proj_dir, "dist", product_name)
51+
archive_path = osp.join(proj_dir, "dist", f"{product_name}-files.zip")
5352
wxs_path = osp.join(wix_dir, f"generic-{product_name}.wxs")
5453
output_path = osp.join(wix_dir, f"{product_name}-{version}.wxs")
5554

56-
dir_ids: dict[str, str] = {}
57-
file_ids: dict[str, str] = {}
58-
59-
files_dict: dict[str, list[str]] = {}
60-
61-
dir_str_list: list[str] = []
62-
comp_str_list: list[str] = []
63-
64-
for pth in os.listdir(dist_dir):
65-
root_dir = osp.join(dist_dir, pth)
66-
67-
if not osp.isdir(root_dir):
68-
continue
69-
70-
dpath_list : list[str] = []
71-
for root, dirs, filenames in os.walk(root_dir):
72-
for dpath in dirs:
73-
relpath = osp.relpath(osp.join(root, dpath), proj_dir)
74-
dpath_list.append(relpath)
75-
dir_ids[relpath] = generate_id()
76-
files_dict.setdefault(osp.dirname(relpath), [])
77-
for filename in filenames:
78-
relpath = osp.relpath(osp.join(root, filename), proj_dir)
79-
file_ids[relpath] = generate_id()
80-
files_dict.setdefault(osp.dirname(relpath), []).append(relpath)
81-
82-
# Create the base directory structure in XML:
83-
base_name = osp.basename(root_dir)
84-
base_path = osp.relpath(root_dir, proj_dir)
85-
base_id = dir_ids[base_path] = generate_id()
86-
dir_xml = ET.Element("Directory", Id=base_id, Name=base_name)
87-
88-
# Nesting directories, recursively, in XML:
89-
for dpath in sorted(dpath_list):
90-
dname = osp.basename(dpath)
91-
parent = dir_xml
92-
for element in parent.iter():
93-
if element.get("Id") == dir_ids[osp.dirname(dpath)]:
94-
parent = element
95-
break
96-
else:
97-
raise ValueError(f"Parent directory not found for {dpath}")
98-
ET.SubElement(parent, "Directory", Id=dir_ids[dpath], Name=dname)
99-
space = " " * 4
100-
ET.indent(dir_xml, space=space, level=4)
101-
dir_str_list.append(space * 4 + ET.tostring(dir_xml, encoding="unicode"))
102-
103-
# Create additionnal components for each file in the directory structure:
104-
for dpath in sorted([base_path] + dpath_list):
105-
did = dir_ids[dpath]
106-
files = files_dict.get(dpath, [])
107-
if files:
108-
# This is a directory with files, so we need to create components:
109-
for path in files:
110-
fid = file_ids[path]
111-
guid = str(uuid.uuid4())
112-
comp_xml = ET.Element("Component", Id=fid, Directory=did, Guid=guid)
113-
ET.SubElement(comp_xml, "File", Source=path, KeyPath="yes")
114-
ET.indent(comp_xml, space=space, level=3)
115-
comp_str = space * 3 + ET.tostring(comp_xml, encoding="unicode")
116-
comp_str_list.append(comp_str)
117-
elif dpath != base_path:
118-
# This is an empty directory, so we need to create a folder:
119-
guid = str(uuid.uuid4())
120-
cdid = f"CreateFolder_{did}"
121-
comp_xml = ET.Element("Component", Id=cdid, Directory=did, Guid=guid)
122-
ET.SubElement(comp_xml, "CreateFolder")
123-
ET.indent(comp_xml, space=space, level=3)
124-
comp_str = space * 3 + ET.tostring(comp_xml, encoding="unicode")
125-
comp_str_list.append(comp_str)
126-
127-
dir_str = "\n".join(dir_str_list).replace("><", ">\n<")
128-
# print("Directory structure:\n", dir_str)
129-
comp_str = "\n".join(comp_str_list).replace("><", ">\n<")
130-
# print("Component structure:\n", comp_str)
55+
# Check if archive exists
56+
if not osp.exists(archive_path):
57+
print(f"ERROR: Archive not found at: {archive_path}")
58+
print(f"Working directory: {os.getcwd()}")
59+
print(f"Project directory: {proj_dir}")
60+
raise FileNotFoundError(f"Archive not found: {archive_path}")
61+
62+
# Get archive size for statistics
63+
archive_size_mb = osp.getsize(archive_path) / (1024 * 1024)
64+
65+
# Count files in dist directory for statistics
66+
total_files = sum(len(files) for _, _, files in os.walk(dist_dir))
67+
68+
print("Archive-based installer mode:")
69+
print(f" Archive: {osp.basename(archive_path)}")
70+
print(f" Archive size: {archive_size_mb:.1f} MB")
71+
print(f" Total files archived: {total_files}")
72+
print(" Components in MSI: ~15 (executables + archive + cleanup)")
13173

13274
# Create the .wxs file:
13375
with open(wxs_path, "r", encoding="utf-8") as fd:
13476
wxs = fd.read()
135-
wxs = insert_text_after(dir_str, "<!-- Automatically inserted directories -->", wxs)
136-
wxs = insert_text_after(comp_str, "<!-- Automatically inserted components -->", wxs)
77+
78+
# Replace version placeholder
13779
wxs = wxs.replace("{version}", version)
80+
wxs = wxs.replace("{archive_path}", f"dist\\{product_name}-files.zip")
81+
13882
with open(output_path, "w", encoding="utf-8") as fd:
13983
fd.write(wxs)
14084
print("Successfully created:", output_path)

0 commit comments

Comments
 (0)