Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/build_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ jobs:
id: get-exe-filename
shell: pwsh
run: |
$baseName = (Get-ChildItem -Path dist-installer -Filter *.exe).BaseName
$baseName = (Get-ChildItem -Path installer/dist-installer -Filter *.exe).BaseName
$unsignedName = "$baseName-unsigned.exe"
# The final name depends on whether this is a release build
$finalName = if ('${{ inputs.release }}' -eq 'true') { "$baseName.exe" } else { $unsignedName }
# Rename the built file to have the "-unsigned" suffix
Rename-Item -Path "dist-installer\$baseName.exe" -NewName $unsignedName
Rename-Item -Path "installer/dist-installer\$baseName.exe" -NewName $unsignedName
echo "unsigned-exe-file-name=$unsignedName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
echo "final-exe-file-name=$finalName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append

Expand All @@ -128,7 +128,7 @@ jobs:
endpoint: 'https://wus2.codesigning.azure.net/'
trusted-signing-account-name: 'LE-Trusted-Signing-Acct'
certificate-profile-name: 'LE-Windows-Certificates'
files-folder: dist-installer
files-folder: installer/dist-installer
files-folder-filter: ${{ steps.get-exe-filename.outputs.unsigned-exe-file-name }}
file-digest: SHA256
timestamp-rfc3161: 'http://timestamp.acs.microsoft.com'
Expand All @@ -138,11 +138,10 @@ jobs:
if: ${{ inputs.release }}
shell: pwsh
run: |
# Only for a release, rename the file from "-unsigned.exe" to its final name after signing.
Rename-Item -Path "dist-installer\${{ steps.get-exe-filename.outputs.unsigned-exe-file-name }}" -NewName "${{ steps.get-exe-filename.outputs.final-exe-file-name }}"
Rename-Item -Path "installer/dist-installer\${{ steps.get-exe-filename.outputs.unsigned-exe-file-name }}" -NewName "${{ steps.get-exe-filename.outputs.final-exe-file-name }}"

- name: Upload installer artifact
uses: actions/upload-artifact@v5
with:
name: ${{ steps.get-exe-filename.outputs.final-exe-file-name }}
path: dist-installer/${{ steps.get-exe-filename.outputs.final-exe-file-name }}
path: installer/dist-installer/${{ steps.get-exe-filename.outputs.final-exe-file-name }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,8 @@ whl
package
kolibrisrc
hooks/kolibri_plugins_entrypoints_hook.py
installer/MicrosoftEdgeWebView2RuntimeInstallerX64.exe
installer/nssm.exe
update_report.txt
installer/dist-installer/
installer/translations/locale/*/*.isl
97 changes: 82 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ build-dmg: needs-version
.PHONY: webview2
# Download WebView2 runtime installer
webview2:
@if [ ! -f MicrosoftEdgeWebView2RuntimeInstallerX64.exe ]; then \
@if [ ! -f installer/MicrosoftEdgeWebView2RuntimeInstallerX64.exe ]; then \
echo "Downloading WebView2 full installer..."; \
( \
trap 'echo "Interrupted. Cleaning up..."; rm -f MicrosoftEdgeWebView2RuntimeInstallerX64.exe; exit 1' INT TERM; \
wget https://go.microsoft.com/fwlink/?linkid=2124701 -O MicrosoftEdgeWebView2RuntimeInstallerX64.exe || { \
trap 'echo "Interrupted. Cleaning up..."; rm -f installer/MicrosoftEdgeWebView2RuntimeInstallerX64.exe; exit 1' INT TERM; \
wget https://go.microsoft.com/fwlink/?linkid=2124701 -O installer/MicrosoftEdgeWebView2RuntimeInstallerX64.exe || { \
echo "\Download failed. Cleaning up..."; \
rm -f MicrosoftEdgeWebView2RuntimeInstallerX64.exe; \
rm -f installer/MicrosoftEdgeWebView2RuntimeInstallerX64.exe; \
exit 1; \
} \
); \
Expand All @@ -107,39 +107,106 @@ webview2:
.PHONY: nssm
# Download NSSM for Windows service management
nssm:
@if [ ! -f nssm.exe ]; then \
@if [ ! -f installer/nssm.exe ]; then \
echo "Downloading NSSM..."; \
( \
trap 'echo "Interrupted. Cleaning up..."; rm -f nssm.zip; rm -rf nssm; exit 1' INT TERM; \
mkdir -p nssm && \
wget https://nssm.cc/release/nssm-$(NSSM_VERSION).zip -O nssm.zip || { \
trap 'echo "Interrupted. Cleaning up..."; rm -f installer/nssm.zip; rm -rf installer/nssm; exit 1' INT TERM; \
mkdir -p installer/nssm && \
wget https://nssm.cc/release/nssm-$(NSSM_VERSION).zip -O installer/nssm.zip || { \
echo "Download failed. Cleaning up..."; \
rm -f nssm.zip; rm -rf nssm; \
rm -f installer/nssm.zip; rm -rf installer/nssm; \
exit 1; \
}; \
unzip -n nssm.zip -d nssm || { \
unzip -n installer/nssm.zip -d installer/nssm || { \
echo "Unzip failed. Cleaning up..."; \
rm -f nssm.zip; rm -rf nssm; \
rm -f installer/nssm.zip; rm -rf installer/nssm; \
exit 1; \
}; \
cp nssm/nssm-$(NSSM_VERSION)/win64/nssm.exe . && \
rm -rf nssm nssm.zip \
cp installer/nssm/nssm-$(NSSM_VERSION)/win64/nssm.exe installer/ && \
rm -rf installer/nssm installer/nssm.zip \
); \
else \
echo "NSSM already present."; \
fi

# Windows Installer Build
.PHONY: build-installer-windows
build-installer-windows: needs-version nssm webview2
build-installer-windows: translations-compile needs-version nssm webview2
ifeq ($(OS),Windows_NT)
# Assumes Inno Setup is installed in the default location.
# MSYS_NO_PATHCONV=1 prevents Git Bash/MINGW from converting the /D flag into a file path.
MSYS_NO_PATHCONV=1 "C:\Program Files (x86)\Inno Setup 6\iscc.exe" /DAppVersion=$(KOLIBRI_VERSION) kolibri.iss
MSYS_NO_PATHCONV=1 "C:\Program Files (x86)\Inno Setup 6\iscc.exe" /DAppVersion=$(KOLIBRI_VERSION) installer/kolibri.iss
else
@echo "Windows installer can only be built on Windows."
endif

INNO_DEFAULT_ISL ?= C:/Program Files (x86)/Inno Setup 6/Default.isl
INNO_LANGUAGES_DIR ?= C:/Program Files (x86)/Inno Setup 6/Languages

TRANSLATIONS_DIR := installer/translations
LOCALE_DIR := $(TRANSLATIONS_DIR)/locale
TEMPLATE_ISL := $(TRANSLATIONS_DIR)/en.isl
SCRIPT_ISL_TO_PO := $(TRANSLATIONS_DIR)/isl_to_po.py
SCRIPT_PO_TO_ISL := $(TRANSLATIONS_DIR)/po_to_isl.py

# New Language Target
# Usage: make new-language LANG=es_ES
.PHONY: new-language
new-language:
$(MAKE) guard-LANG
@echo "Scaffolding new PO file for locale '$(LANG)'..."
$(PYTHON_EXEC) $(SCRIPT_ISL_TO_PO) \
--template $(TEMPLATE_ISL) \
--output $(LOCALE_DIR)/$(LANG)/messages.po \
--lang "$(LANG)" \
--inno-dir "$(INNO_LANGUAGES_DIR)" \
--no-overwrite

# Export Source Target (en)
.PHONY: translations-export-source
translations-export-source:
@echo "Exporting master en.isl to locale/en/messages.po (Source)..."
$(PYTHON_EXEC) $(SCRIPT_ISL_TO_PO) \
--template $(TEMPLATE_ISL) \
--output $(LOCALE_DIR)/en/messages.po \
--lang "en"

# Compile Target (PO -> ISL)
.PHONY: translations-compile
translations-compile:
@echo "Compiling PO files to ISL format..."
@# Loop through directories in translations/locale/
@for lang_dir in $(LOCALE_DIR)/*; do \
if [ -d "$$lang_dir" ]; then \
lang_code=$$(basename "$$lang_dir"); \
\
# Skip 'en' because we use the master en.isl template directly \
if [ "$$lang_code" = "en" ]; then \
continue; \
fi; \
\
po_file="$$lang_dir/messages.po"; \
isl_file="$$lang_dir/$${lang_code}.isl"; \
\
if [ -f "$$po_file" ]; then \
echo " -> Processing $$lang_code ..."; \
$(PYTHON_EXEC) $(SCRIPT_PO_TO_ISL) \
-t $(TEMPLATE_ISL) \
-i "$$po_file" \
-o "$$isl_file" \
-l "$$lang_code"; \
fi \
fi \
done

.PHONY: update-translations
update-translations:
@echo "Updating master language file from '$(INNO_DEFAULT_ISL)'..."
$(PYTHON_EXEC) installer/translations/update_from_inno_default.py \
--new-default "$(INNO_DEFAULT_ISL)" \
--project-master "$(TEMPLATE_ISL)"
@echo "Update complete. Please review update_report.txt and commit the changes to en.isl."

compile-mo:
find src/kolibri_app/locales -name LC_MESSAGES -exec msgfmt {}/wxapp.po -o {}/wxapp.mo \;

Expand Down
1 change: 1 addition & 0 deletions build_requires.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pkginfo
PyInstaller==6.16.0
dmgbuild==1.6.5; sys_platform == 'darwin'
attrdict==2.0.1 # required to install wxpython
polib==1.2.0 # required for handling .po translation files
59 changes: 47 additions & 12 deletions kolibri.iss → installer/kolibri.iss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#ifndef AppVersion
#error "The AppVersion definition must be passed to the compiler via the command line, e.g., /DAppVersion=x.y.z"
#endif
#define SourceDir "dist\" + AppName + "-" + AppVersion
#define SourceDir "..\dist\" + AppName + "-" + AppVersion
#define KolibriDataDir "{commonappdata}\kolibri"
#define NssmExePath "{app}\nssm\nssm.exe"
#define TaskkillExePath "{sys}\taskkill.exe"
Expand Down Expand Up @@ -38,6 +38,42 @@ ArchitecturesInstallIn64BitMode=x64compatible
UninstallDisplayIcon={app}\{#AppExeName}
SetupLogging=yes
CloseApplicationsFilter={#AppExeName}
ShowLanguageDialog=yes

[Languages]
Name: "en"; MessagesFile: "translations\en.isl"
; Name: "ar"; MessagesFile: "translations\locale\ar\ar.isl"
; Name: "bg_bg"; MessagesFile: "translations\locale\bg-bg\bg-bg.isl"
; Name: "bn_bd"; MessagesFile: "translations\locale\bn-bd\bn-bd.isl"
Name: "de"; MessagesFile: "translations\locale\de\de.isl"
; Name: "el"; MessagesFile: "translations\locale\el\el.isl"
; Name: "es_es"; MessagesFile: "translations\locale\es-es\es-es.isl"
; Name: "es_419"; MessagesFile: "translations\locale\es-419\es-419.isl"
; Name: "fa"; MessagesFile: "translations\locale\fa\fa.isl"
; Name: "fr_fr"; MessagesFile: "translations\locale\fr-fr\fr-fr.isl"
; Name: "ff_cm"; MessagesFile: "translations\locale\ff-cm\ff-cm.isl"
; Name: "gu_in"; MessagesFile: "translations\locale\gu-in\gu-in.isl"
; Name: "ha"; MessagesFile: "translations\locale\ha\ha.isl"
; Name: "hi_in"; MessagesFile: "translations\locale\hi-in\hi-in.isl"
; Name: "ht"; MessagesFile: "translations\locale\ht\ht.isl"
; Name: "id"; MessagesFile: "translations\locale\id\id.isl"
; Name: "it"; MessagesFile: "translations\locale\it\it.isl"
; Name: "ka"; MessagesFile: "translations\locale\ka\ka.isl"
; Name: "km"; MessagesFile: "translations\locale\km\km.isl"
; Name: "ko"; MessagesFile: "translations\locale\ko\ko.isl"
; Name: "mr"; MessagesFile: "translations\locale\mr\mr.isl"
; Name: "my"; MessagesFile: "translations\locale\my\my.isl"
; Name: "ny"; MessagesFile: "translations\locale\ny\ny.isl"
; Name: "pa"; MessagesFile: "translations\locale\pa\pa.isl"
; Name: "pt_br"; MessagesFile: "translations\locale\pt-br\pt-br.isl"
; Name: "pt_mz"; MessagesFile: "translations\locale\pt-mz\pt-mz.isl"
; Name: "sw_tz"; MessagesFile: "translations\locale\sw-tz\sw-tz.isl"
; Name: "te"; MessagesFile: "translations\locale\te\te.isl"
; Name: "uk"; MessagesFile: "translations\locale\uk\uk.isl"
; Name: "ur_pk"; MessagesFile: "translations\locale\ur-pk\ur-pk.isl"
; Name: "vi"; MessagesFile: "translations\locale\vi\vi.isl"
; Name: "yo"; MessagesFile: "translations\locale\yo\yo.isl"
; Name: "zh_hans"; MessagesFile: "translations\locale\zh-hans\zh-hans.isl"

[Registry]
; This registry key is used to detect the installed version for upgrades/repairs.
Expand All @@ -47,7 +83,7 @@ Root: HKLM; Subkey: "Software\Kolibri"; ValueType: dword; ValueName: "ShowTrayIc
Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueName: "KolibriTray"; Flags: uninsdeletevalue

[Tasks]
Name: "installservice"; Description: "Run Kolibri automatically when the computer starts"; GroupDescription: "Installation Type:";
Name: "installservice"; Description: "{cm:InstallServiceTask}"; GroupDescription: "{cm:InstallationType}";
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";

[Dirs]
Expand Down Expand Up @@ -210,14 +246,14 @@ begin
begin
Log('ERROR: User data migration failed. Robocopy exit code: ' + IntToStr(ResultCode));
// Even if robocopy runs but fails, we should inform the user.
MsgBox('Kolibri was unable to automatically move your user data to the new location. Please move the contents of "' + SourcePath + '" to "' + DestPath + '" manually.', mbError, MB_OK);
MsgBox(FmtMessage(CustomMessage('MigrationFailed'), [SourcePath, DestPath]), mbError, MB_OK);
end
end
else
begin
// This block runs if robocopy.exe itself could not be found or executed.
Log('ERROR: Failed to execute robocopy.exe. Ensure it is in the system PATH. Error code: ' + IntToStr(ResultCode));
MsgBox('Kolibri was unable to automatically move your user data to the new location. Please move the contents of "' + SourcePath + '" to "' + DestPath + '" manually.', mbError, MB_OK);
MsgBox(FmtMessage(CustomMessage('MigrationFailed'), [SourcePath, DestPath]), mbError, MB_OK);
end;
end;

Expand All @@ -234,14 +270,14 @@ begin
// if the process fails to launch
Log(Format('ERROR: Failed to launch process for "%s". System Error: %s', [Description, SysErrorMessage(ResultCode)]));
if not WizardSilent() then
MsgBox(Format('A critical error occurred while trying to run a setup command: %s.'#13#10'The installation cannot continue.', [Description]), mbError, MB_OK);
MsgBox(FmtMessage(CustomMessage('CriticalError'), [Description]), mbError, MB_OK);
Abort;
end;
if ResultCode <> 0 then
begin
Log(Format('ERROR: Command "%s" failed with a non-zero exit code: %d.', [Description, ResultCode]));
if not WizardSilent() then
MsgBox(Format('A command required for setup failed to execute correctly: %s.'#13#10'Error Code: %d'#13#10'The installation cannot continue.', [Description, ResultCode]), mbError, MB_OK);
MsgBox(FmtMessage(CustomMessage('CommandError'), [Description, IntToStr(ResultCode)]), mbError, MB_OK);
Abort;
end
else
Expand Down Expand Up @@ -305,7 +341,7 @@ begin
Log(Format(' -> Installer Version String: "%s"', [InstallerVersionString]));
Log(Format(' -> Installed Version String: "%s"', [InstalledVersionString]));
if not WizardSilent() then
MsgBox('Could not compare versions due to an invalid version format. Please uninstall the previous version manually and try again.', mbError, MB_OK);
MsgBox(CustomMessage('VersionParseError'), mbError, MB_OK);
Result := False;
Exit;
end;
Expand All @@ -319,15 +355,15 @@ begin
begin
Log(Format('Downgrade detected. Installed version %s is newer than installer version %s. Aborting.', [InstalledVersionString, InstallerVersionString]));
if not WizardSilent() then
MsgBox(Format('A newer version of {#AppName} (%s) is already installed.' + #13#10#13#10 + 'This installer contains version %s, which is older than the installed version.' + #13#10 + 'The setup will now exit.', [InstalledVersionString, InstallerVersionString]), mbInformation, MB_OK);
MsgBox(FmtMessage(CustomMessage('NewerVersionInstalled'), [InstalledVersionString, InstallerVersionString]), mbInformation, MB_OK);
Result := False;
end
else if VersionDiff = 0 then
begin
Log('Same version detected. Proposing a repair/reinstall.');
if not WizardSilent() then
begin
if MsgBox('This version of {#AppName} is already installed.' + #13#10#13#10 + 'Do you want to repair the installation by reinstalling it?', mbConfirmation, MB_YESNO) <> IDYES then
if MsgBox(FmtMessage(CustomMessage('SameVersionInstalled'), ['{#AppName}']), mbConfirmation, MB_YESNO) <> IDYES then
Result := False;
end;
end
Expand All @@ -336,7 +372,7 @@ begin
Log('Older version detected. Proposing an upgrade.');
if not WizardSilent() then
begin
if MsgBox(Format('An older version of {#AppName} (%s) was detected.' + #13#10#13#10 + 'Do you want to upgrade to version {#AppVersion}?', [InstalledVersionString]), mbConfirmation, MB_YESNO) <> IDYES then
if MsgBox(FmtMessage(CustomMessage('OlderVersionInstalled'), [InstalledVersionString]), mbConfirmation, MB_YESNO) <> IDYES then
Result := False;
end;
end;
Expand Down Expand Up @@ -527,8 +563,7 @@ begin
// Default to NOT deleting user data unless the user explicitly agrees.
g_DeleteUserData := False;

if MsgBox('Do you want to completely remove all Kolibri user data?' + #13#10 +
'This includes all downloaded content, user accounts, and progress, and cannot be undone.',
if MsgBox(CustomMessage('ConfirmUninstallData'),
mbConfirmation, MB_YESNO) = IDYES then
begin
g_DeleteUserData := True;
Expand Down
Loading