-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild-windows.py
More file actions
404 lines (334 loc) · 15 KB
/
build-windows.py
File metadata and controls
404 lines (334 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
#!/usr/bin/env python3
import os
import shutil
import subprocess
import zipfile
from pathlib import Path
import re
import xml.etree.ElementTree as ET
def get_version_input():
"""Get version number from user or use current version from .csproj"""
print("\n📦 Version Configuration")
print("=" * 50)
current_version = get_current_version()
if current_version:
print(f"Current version in .csproj: {current_version}")
version = input(f"Enter version number (e.g., 1.0.1) or press Enter to use current [{current_version or '1.0.0'}]: ").strip()
if not version:
version = current_version or "1.0.0"
if not re.match(r'^\d+\.\d+\.\d+$', version):
print(f"⚠️ Invalid version format. Using default: 1.0.0")
version = "1.0.0"
return version
def get_current_version():
"""Read current version from .csproj file"""
try:
tree = ET.parse("./AkademiTrack.csproj")
root = tree.getroot()
for prop_group in root.findall('.//PropertyGroup'):
version_elem = prop_group.find('Version')
if version_elem is not None and version_elem.text:
return version_elem.text.strip()
return None
except Exception as e:
print(f"⚠️ Could not read version from .csproj: {e}")
return None
def update_csproj_version(version):
"""Update version in .csproj file"""
try:
tree = ET.parse("./AkademiTrack.csproj")
root = tree.getroot()
version_updated = False
for prop_group in root.findall('.//PropertyGroup'):
version_elem = prop_group.find('Version')
if version_elem is not None:
version_elem.text = version
version_updated = True
break
if not version_updated:
prop_groups = root.findall('.//PropertyGroup')
if prop_groups:
version_elem = ET.SubElement(prop_groups[0], 'Version')
version_elem.text = version
version_updated = True
if version_updated:
tree.write("./AkademiTrack.csproj", encoding='utf-8', xml_declaration=True)
print(f"✅ Updated .csproj version to {version}")
return True
else:
print(f"⚠️ Could not update version in .csproj")
return False
except Exception as e:
print(f"❌ Failed to update .csproj: {e}")
return False
def build_windows_release(version):
"""Build Windows release - creates exe and portable zip"""
print(f"\n🏗️ Building AkademiTrack for Windows (x64)...")
print("=" * 50)
# Directories
publish_dir = Path("./publish-win")
publish_single = Path("./publish-win-single")
release_folder = Path(f"./Releases/v{version}")
# Check for icon file
icon_path = Path("./Assets/AT-1024.ico")
if not icon_path.exists():
print(f"⚠️ Icon file not found: {icon_path}")
print("Checking for alternative formats...")
png_icon = Path("./Assets/AT-1024.png")
if png_icon.exists():
print(f"⚠️ Found PNG but .ico format is preferred")
print("Please convert AT-1024.png to AT-1024.ico")
icon_path = None
else:
icon_size = icon_path.stat().st_size
print(f"✅ Icon file found: {icon_path} ({icon_size} bytes)")
# Check for splash image
splash_path = None
for ext in ['.png', '.jpg', '.jpeg', '.gif']:
splash_candidate = Path(f"./Assets/splash{ext}")
if splash_candidate.exists():
splash_path = splash_candidate
splash_size = splash_path.stat().st_size / 1024
print(f"✅ Splash image found: {splash_path} ({splash_size:.1f} KB)")
break
if not splash_path:
print(f"⚠️ No splash image found (looked for Assets/splash.png/jpg/gif)")
print("💡 Create a splash screen for a more professional installer!")
# Clean directories
if publish_dir.exists():
print(f"🧹 Cleaning publish directory...")
shutil.rmtree(publish_dir)
if publish_single.exists():
print(f"🧹 Cleaning single-file directory...")
shutil.rmtree(publish_single)
if release_folder.exists():
print(f"🧹 Cleaning release folder...")
shutil.rmtree(release_folder)
release_folder.mkdir(parents=True, exist_ok=True)
# Step 1: Publish for distribution (multi-file)
print(f"\n📦 Step 1: Publishing for distribution (multi-file)...")
publish_cmd = [
"dotnet", "publish",
"-c", "Release",
"--self-contained",
"-r", "win-x64",
"-o", str(publish_dir),
"-p:PublishSingleFile=false"
]
print(f"Running: {' '.join(publish_cmd)}")
result = subprocess.run(publish_cmd, capture_output=True, text=True,
encoding='utf-8', errors='replace')
if result.returncode != 0:
print(f"❌ Publish failed: {result.stderr}")
return False
print("✅ Published for distribution successfully")
# Step 2: Publish single file exe (for standalone distribution)
print(f"\n📦 Step 2: Publishing standalone single-file exe...")
publish_single_cmd = [
"dotnet", "publish",
"-c", "Release",
"--self-contained",
"-r", "win-x64",
"-o", str(publish_single),
"-p:PublishSingleFile=true",
"-p:IncludeNativeLibrariesForSelfExtract=true",
"-p:EnableCompressionInSingleFile=true"
]
print(f"Running: {' '.join(publish_single_cmd)}")
result = subprocess.run(publish_single_cmd, capture_output=True, text=True,
encoding='utf-8', errors='replace')
if result.returncode != 0:
print(f"❌ Single-file publish failed: {result.stderr}")
return False
print("✅ Published single-file exe successfully")
# Verify single-file executable exists and is substantial
exe_single = publish_single / "AkademiTrack.exe"
if not exe_single.exists():
print(f"❌ Single-file executable not found: {exe_single}")
return False
exe_size = exe_single.stat().st_size / 1024 / 1024
print(f"✅ Single-file executable: {exe_single.name} ({exe_size:.1f} MB)")
if exe_size < 10:
print(f"⚠️ Warning: Executable seems too small ({exe_size:.1f} MB). This might be a stub, not a full exe.")
# Step 3: Create portable ZIP from multi-file build
print(f"\n📦 Step 3: Creating portable ZIP...")
portable_zip = release_folder / f"AkademiTrack-win-Portable.zip"
try:
with zipfile.ZipFile(portable_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
file_count = 0
for file_path in publish_dir.rglob('*'):
if file_path.is_file():
arc_name = file_path.relative_to(publish_dir)
zipf.write(file_path, arc_name)
file_count += 1
print(f"✅ Added {file_count} files to portable ZIP")
except Exception as e:
print(f"❌ Failed to create portable ZIP: {e}")
return False
portable_size = portable_zip.stat().st_size / 1024 / 1024
print(f"✅ Portable ZIP created: {portable_zip.name} ({portable_size:.1f} MB)")
# Step 4: Create Velopack release package
print(f"\n📦 Step 4: Creating Velopack release package...")
try:
# Create releases directory
releases_dir = Path("./Releases")
releases_dir.mkdir(exist_ok=True)
# Use vpk to pack the app
vpk_cmd = [
"vpk", "pack",
"--packId", "AkademiTrack",
"--packVersion", version,
"--packDir", str(publish_dir),
"--outputDir", str(releases_dir),
"--runtime", "win-x64",
"--mainExe", "AkademiTrack.exe"
]
# Add icon if it exists
if icon_path and icon_path.exists():
vpk_cmd.extend(["--icon", str(icon_path)])
print(f"Running: {' '.join(vpk_cmd)}")
result = subprocess.run(vpk_cmd, capture_output=True, text=True,
encoding='utf-8', errors='replace')
if result.returncode == 0:
# Find the created release file
release_files = list(releases_dir.glob(f"AkademiTrack-{version}-*.nupkg"))
if release_files:
release_file = release_files[0]
velopack_size = release_file.stat().st_size / 1024 / 1024
print(f"✅ Velopack release created: {release_file.name} ({velopack_size:.1f} MB)")
# Copy to release folder
shutil.copy2(release_file, release_folder / release_file.name)
# Also copy RELEASES file if it exists
releases_file = releases_dir / "RELEASES"
if releases_file.exists():
shutil.copy2(releases_file, release_folder / "RELEASES")
print(f"✅ RELEASES file copied")
else:
print("⚠️ Velopack release file not found")
else:
print(f"⚠️ Velopack packaging failed: {result.stderr}")
print("💡 Make sure 'vpk' tool is installed: dotnet tool install -g vpk")
except Exception as e:
print(f"⚠️ Velopack packaging failed: {e}")
print("💡 Make sure 'vpk' tool is installed: dotnet tool install -g vpk")
# Step 5: Copy the standalone single-file EXE to release folder
print(f"\n📦 Step 5: Adding standalone single-file EXE...")
standalone_exe = release_folder / "AkademiTrack.exe"
shutil.copy2(exe_single, standalone_exe)
standalone_size = standalone_exe.stat().st_size / 1024 / 1024
print(f"✅ Standalone single-file EXE: {standalone_exe.name} ({standalone_size:.1f} MB)")
# Verify the release folder exists and has files
if not release_folder.exists():
print(f"❌ Release folder was not created!")
return False
files_in_release = list(release_folder.iterdir())
if not files_in_release:
print(f"❌ Release folder is empty!")
return False
return release_folder
def clear_icon_cache():
"""Clear Windows icon cache to show updated icons immediately"""
print("\n🔄 Clearing Windows icon cache...")
print("=" * 50)
try:
import platform
if platform.system() != 'Windows':
print("⏭️ Not on Windows, skipping icon cache clear")
return
# Kill Explorer to release icon cache files
subprocess.run(["taskkill", "/F", "/IM", "explorer.exe"],
capture_output=True, check=False)
# Delete icon cache files
localappdata = os.environ.get('LOCALAPPDATA')
if localappdata:
cache_files = [
os.path.join(localappdata, "IconCache.db"),
os.path.join(localappdata, "Microsoft", "Windows", "Explorer", "iconcache_*.db")
]
deleted_count = 0
for pattern in cache_files:
if '*' in pattern:
import glob
for file in glob.glob(pattern):
try:
os.remove(file)
deleted_count += 1
except:
pass
else:
try:
if os.path.exists(pattern):
os.remove(pattern)
deleted_count += 1
except:
pass
if deleted_count > 0:
print(f"✅ Deleted {deleted_count} icon cache file(s)")
else:
print("ℹ️ No icon cache files found to delete")
# Restart Explorer
subprocess.Popen(["explorer.exe"])
import time
time.sleep(1)
print("✅ Icon cache cleared - new icons should display correctly")
print("💡 If icons still look blurry, try restarting your PC")
except Exception as e:
print(f"⚠️ Could not clear icon cache: {e}")
print("💡 You can manually restart Explorer or reboot to see new icons")
def main():
print("🚀 AkademiTrack Windows Build & Package Tool")
print("=" * 50)
print("📧 Contact: cyberbrothershq@gmail.com")
print("🌐 Website: https://cybergutta.github.io/CG/")
print("💻 GitHub: https://github.com/CyberGutta/AkademiTrack")
print("=" * 50)
# Get version number
version = get_version_input()
print(f"\n📌 Using version: {version}")
# Ask if user wants to update .csproj
update_proj = input("\nUpdate version in .csproj file? (y/n) [n]: ").strip().lower()
if update_proj == 'y':
update_csproj_version(version)
# Build everything
release_folder = build_windows_release(version)
if release_folder:
print("\n" + "=" * 50)
print("🎉 Build completed successfully!")
print(f"📦 Version: {version}")
print(f"\n📁 Release folder: {release_folder}/")
print("\n📋 Contents:")
# List all files in release folder
for item in sorted(release_folder.iterdir()):
if item.is_file():
size = item.stat().st_size / 1024 / 1024
print(f" ✅ {item.name} ({size:.1f} MB)")
print("\n📋 Files created:")
print(f" • AkademiTrack.exe - Standalone single-file executable (RUN THIS ONE!)")
print(f" • AkademiTrack-win-Portable.zip - Portable ZIP package")
print(f" • AkademiTrack-{version}-*.nupkg - Velopack release package (for auto-updates)")
print(f" • RELEASES - Velopack releases index file")
print("\n💡 Next steps:")
print(f" • Test AkademiTrack.exe from {release_folder}/")
print(f" • Distribute the portable ZIP for manual installs")
print(f" • Use the .nupkg + RELEASES for Velopack auto-updates")
print(f" • Upload to your release server/CDN")
print("\n🎨 Customization tips:")
print(f" • Create Assets/splash.png (400x300 or 600x400) for installer splash screen")
print(f" • Include your logo, tagline, and 'By CyberGutta' credit")
print(f" • For animated splash, use .gif format")
print(f" • Make sure Assets/AT-1024.ico exists (multiple sizes: 16,32,48,256)")
# Clear icon cache at the end
clear_icon_cache()
print("\n✨ Built with ❤️ by CyberGutta")
print(" Andreas Nilsen & Mathias Hansen")
else:
print("\n❌ Build failed!")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n⚠️ Build interrupted by user")
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
import traceback
traceback.print_exc()