Skip to content

Commit

Permalink
add innosetup Windows installer
Browse files Browse the repository at this point in the history
The new installer supports portable installs, adding to $env:PATH, and
sets the documentation read only.
  • Loading branch information
guijan committed Jan 2, 2025
1 parent 912dc70 commit b272f54
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 202 deletions.
18 changes: 6 additions & 12 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022-2024 Guilherme Janczak <guilherme.janczak@yandex.com>
# Copyright (c) 2022-2025 Guilherme Janczak <guilherme.janczak@yandex.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
Expand Down Expand Up @@ -168,19 +168,13 @@ jobs:
path-type: strict
pacboy: |
gcc:p meson:p ninja:p dos2unix: git: groff:
# Some environments have no nsis, so we use mingw-w64's.
- name: env-lacks-nsis
if: matrix.sys == 'msys' || matrix.sys == 'clang64'
run: pacman -S --noconfirm --needed mingw-w64-x86_64-nsis
- name: env-has-nsis
if: matrix.sys != 'msys' && matrix.sys != 'clang64'
run: pacboy -S --noconfirm --needed nsis:p
- uses: actions/checkout@v4.2.2
- name: build
run: |
if [ "$MSYSTEM" = "MSYS" ] || [ "$MSYSTEM" = "CLANG64" ]; then
PATH="${PATH}:/mingw64/bin"
fi
inno="$(cmd //c 'echo %ProgramFiles(x86)%')\\Inno Setup 6\\"
inno="$(cygpath -u "$inno")"
PATH="${PATH}:${inno}"
echo "$PATH"
meson setup build
meson compile -C build
- name: test
Expand Down Expand Up @@ -229,7 +223,7 @@ jobs:
- uses: actions/setup-python@v5.3.0
- run: |
pip install meson
choco install ninja nsis groff -y
choco install ninja innosetup groff -y
- uses: actions/checkout@v4.2.2
- name: Enable ARM64 Developer Command Prompt
if: matrix.arch == 'ARM64'
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2021-2022, 2024 Guilherme Janczak <guilherme.janczak@yandex.com>
Copyright (c) 2021-2025 Guilherme Janczak <guilherme.janczak@yandex.com>

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
Expand Down
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,6 @@ Compile the installer:
PS C:\Users\foo\dictpw> meson setup build && meson compile installer -C build
```

## Windows example
Unfortunately, Microsoft hasn't provided a proper way to install command line
utilities to Windows. The installer registers the .exe in the Windows Registry
which allows running the program from `cmd` using the `start` command:
```console
C:\Users\foo>start /b /wait dictpw
unusual.skewer.swirl.whinny
```
Keep in mind the /b and /wait flags are necessary: they tell cmd.exe not to
start another cmd.exe, and to wait for the program's exit.

## Windows documentation
The installer also installs the manual. Check _dictpw.txt_ inside the
installation directory.
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Changes in dictpw master:
- Fix broken cross compiling
- Integrate the installer with Meson
- Change the installer's compression algorithm to LZMA to improve ratios
- Change to Inno Setup for the Windows installer for improved ARM support
- Set documentation read only on Windows to prevent accidental modification
- Add a portable install option to the Windows installer
- Add an option to add the program to $env:PATH on Windows
58 changes: 39 additions & 19 deletions meson.build
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (c) 2021-2022, 2024 Guilherme Janczak <guilherme.janczak@yandex.com>
# Copyright (c) 2021-2022, 2024-2025
# Guilherme Janczak <guilherme.janczak@yandex.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
Expand Down Expand Up @@ -119,25 +120,24 @@ if host_machine.system() == 'windows' or host_machine.system() == 'cygwin'
capture: true,
build_by_default: true)

inst_cmd = [find_program('iscc'),
'-DBUILDDIR=' + meson.current_build_dir(),
'-Fsetup-dictpw',
'-DMESON',
'-DNAME=' + meson.project_name(),
'-DVERSION=' + meson.project_version(),
'-DURL=https://github.com/guijan/dictpw',
# Inno Setup's ExtractFileName (basename function) expects the
# Windows path separator ('\'), but Meson uses the Unix path
# separator ('/'), so create a basename now.
'-DEXEFILE=' + dictpw.full_path(),
'-DLICENSE=' + license.full_path(),
'-DMANFILE=' + man.full_path(),
'-DREADME=' + readme.full_path(),]

fs = import('fs')
inst_cmd = [find_program('makensis'), '-NOCD',
'-INPUTCHARSET', 'UTF8', '-OUTPUTCHARSET', 'UTF8',
'-XSetCompressor /SOLID /FINAL lzma',
'-DMESON=true',
'-DOUTFILE=setup-dictpw.exe',
'-DEXEFILE=' + fs.name(dictpw.full_path()),
'-DMANFILE=' + fs.name(man.full_path()),
'-DLICENSE=' + fs.name(license.full_path()),
'-DREADME=' + fs.name(readme.full_path()),
# This goes into the string file information block and is
# free-form.
'-DPROJECT_VERSION=' + meson.project_version(),
# This goes into the fixed file information block and is in the
# form of 4 integers separated by a dot, e.g. '1.2.3.4'
# We append '.0' to our SemVer to match that format.
'-DVI_VERSION=' + meson.project_version() + '.0' ]
if libbsd_dep.found() and libbsd_dep.type_name() == 'internal'
inst_cmd += '-DLIBOBSD_LICENSE=true'
inst_cmd += '-DLIBOBSD_LICENSE=subprojects/libobsd/LICENSE_libobsd.txt'
endif
# Programs built in Cygwin and MSYS2's MSYS environment are linked against
# a special DLL with their implementations of Unix inside, distribute it.
Expand All @@ -157,7 +157,27 @@ if host_machine.system() == 'windows' or host_machine.system() == 'cygwin'
error('cygwin/msys2 DLL not found')
endif
endif
# Mapping between:
# https://mesonbuild.com/Reference-tables.html
# https://jrsoftware.org/ishelp/index.php?topic=archidentifiers
meson_to_iscc_arch = {
'arm': 'arm32compatible',
'aarch64': 'arm64',
'x86_64': 'x64compatible',
'x86': 'x86compatible'
}
inst_cmd += '-DARCH=' + meson_to_iscc_arch[host_machine.cpu_family()]
if cc.get_id() == 'msvc'
# https://learn.microsoft.com/en-us/visualstudio/releases/2022/compatibility#build-apps-that-run-on-windows-clients
winmin = '6.1sp1'
else
# https://www.msys2.org/docs/windows_support/
# Minimum package requirement, not minimum toolchain requirement, I'm not
# going to bother figuring out which is right, so use the highest one.
winmin = '6.3'
endif
inst_cmd += '-DWIN_MIN=' + winmin
run_target('installer',
command: inst_cmd + ['--', files('src/dictpw.nsi')],
command: inst_cmd + files('src/dictpw.iss'),
depends: [dictpw, man, license, readme, dll_copy])
endif
21 changes: 20 additions & 1 deletion src/dictpw.1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.\" $OpenBSD: mdoc.template,v 1.15 2014/03/31 00:09:54 dlg Exp $
.\"
.\" Copyright (c) 2021-2022, 2024
.\" Copyright (c) 2021-2022, 2024-2025
.\" Guilherme Janczak <guilherme.janczak@yandex.com
.\"
.\" Permission to use, copy, modify, and distribute this software for any
Expand Down Expand Up @@ -43,6 +43,25 @@ The program rejects values that are nonsensically small or large.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Unix usage:
.Dl $ dictpw
.Pp
Windows users can always call
.Nm
using the the App Paths shortcut.
PowerShell:
.Dl PS C:\eUsers\efoo> Start-Process -NoNewWindow -Wait dictpw
Batch:
.Dl C:\eUsers\efoo> start /b /wait dictpw
.Pp
If a Windows installation of
.Nm
was done via Chocolatey, or if
.Em Add to $env:PATH
(the default) was selected during installation,
the program can be called normally:
.Dl PS C:\eUsers\efoo> dictpw
.Sh STANDARDS
.Lk https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt EFF's long word list .
.Sh AUTHORS
Expand Down
207 changes: 207 additions & 0 deletions src/dictpw.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
; Copyright (c) 2025 Guilherme Janczak <guilherme.janczak@yandex.com>
;
; Permission to use, copy, modify, and distribute this software for any
; purpose with or without fee is hereby granted, provided that the above
; copyright notice and this permission notice appear in all copies.
;
; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

#ifndef MESON
#error "This installer can only be generated with Meson"
#endif

[Setup]
AppId=dictpw_{{sample.judiciary.virus.wildly.grafted.askew.overture.paprika}
AppName={#NAME}
AppVersion={#VERSION}
VersionInfoDescription="generate password from dictionary"
VersionInfoVersion={#VERSION}
; Don't put the version in the Add/Remove Programs entry, that's weird.
UninstallDisplayName={#NAME}
AppCopyright="(c) Guilherme Janczak"
AppPublisherURL={#URL}
AppSupportURL={#URL}
AppUpdatesURL={#URL}
DefaultDirName={autopf}\{#NAME}
ArchitecturesAllowed={#ARCH}
ArchitecturesInstallIn64BitMode=x64compatible
DefaultGroupName={#NAME}
DisableProgramGroupPage=yes
LicenseFile={#LICENSE}
PrivilegesRequiredOverridesAllowed=dialog
SourceDir={#BUILDDIR}
OutputDir=.
Compression=lzma
SolidCompression=yes
WizardStyle=modern
MinVersion={#WIN_MIN}
; Work around the time misdesign typical on Windows.
TimeStampsInUTC=yes
Uninstallable=WizardIsTaskSelected('stationary')
ChangesEnvironment=WizardIsTaskSelected('stationary\env_path')

[Tasks]
Name: stationary; \
Description: "Stationary Installation (creates registry entries and uninstaller)"; \
Flags: exclusive checkablealone
Name: stationary\env_path; Description: "Add to $env:PATH"
; "portable" is just a marker and never used
Name: portable; \
Description: "Portable Installation (no registry entries and no uninstaller)"; \
Flags: unchecked exclusive

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Files]
#define EXEDESTDIR '{app}\bin'
#ifdef MSYS_DLL
Source: "{#MSYS_DLL}"; DestDir: "{#EXEDESTDIR}"; Flags: ignoreversion
#endif
Source: "{#EXEFILE}"; DestDir: "{#EXEDESTDIR}"; Flags: ignoreversion
Source: "{#MANFILE}"; DestDir: "{app}"; \
Flags: ignoreversion overwritereadonly uninsremovereadonly; \
Attribs: readonly
Source: "{#LICENSE}"; DestDir: "{app}"; \
Flags: ignoreversion overwritereadonly uninsremovereadonly; \
Attribs: readonly
Source: "{#README}"; DestDir: "{app}"; \
Flags: isreadme ignoreversion overwritereadonly uninsremovereadonly; \
Attribs: readonly
; LIBOBSD_LICENSE is optional, it may not be needed on msys2 someday.
#ifdef LIBOBSD_LICENSE
Source: "{#LIBOBSD_LICENSE}"; DestDir: "{app}"; \
Flags: ignoreversion overwritereadonly uninsremovereadonly; \
Attribs: readonly
#endif

[Registry]
Root: HKA; \
Subkey: "Software\Microsoft\Windows\CurrentVersion\App Paths\dictpw.exe"; \
Flags: uninsdeletekey; \
ValueType: string; \
ValueData: "{#EXEDESTDIR}\dictpw.exe"; \
Tasks: stationary
#define ENVIRONMENT \
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
Root: HKLM; \
Subkey: "{#ENVIRONMENT}"; \
ValueType: expandsz; \
ValueName: "Path"; \
ValueData: "{olddata};{#EXEDESTDIR}"; \
Check: ShouldIAddToPATH('{#EXEDESTDIR}'); \
Tasks: stationary\env_path

[code]
Function ShouldIAddToPATH(Path: string): boolean;
var
PathList: string;
begin
Result := true;
if RegQueryStringValue(HKLM, '{#ENVIRONMENT}', 'Path', PathList) then
Result := Pos(';' + PathList + ';', ';' + Path + ';') = 0;
end;
Procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
Path, PathList: String;
Position: Integer;
PathLen: Longint;
begin
Path := ExpandConstant('{#EXEDESTDIR}')
If (CurUninstallStep = usPostUninstall) and
{ can't `WizardIsTaskSelected('stationary\env_path') and` in uninstall }
RegQueryStringValue(HKLM, '{#ENVIRONMENT}', 'Path', PathList) then
begin
PathLen := Length(Path)
Position := Pos(Path, PathList)
If Position <> 0 then
begin
Delete(PathList, Position, PathLen);
If Length(PathList) <> 0 then
begin
If PathList[Position-1] = ';' then
Position := Position - 1;
Delete(PathList, Position, 1);
RegWriteExpandStringValue(HKLM, '{#ENVIRONMENT}', 'Path',
PathList);
end;
end;
end;
end;
Function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo,
MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo,
MemoTasksInfo: String): String;
begin
Result := 'If {#NAME} has been installed before, the installer will ';
If not WizardIsTaskSelected('stationary') then
Result := Result + 'NOT'
else
Result := Result + 'first';
Result := Result + ' prompt to uninstall the previous version.' +
Newline + Newline;
if MemoUserInfoInfo <> '' then begin
Result := MemoUserInfoInfo + Newline + NewLine;
end;
if MemoDirInfo <> '' then begin
Result := Result + MemoDirInfo + Newline + NewLine;
end;
if MemoTypeInfo <> '' then begin
Result := Result + MemoTypeInfo + Newline + NewLine;
end;
if MemoComponentsInfo <> '' then begin
Result := Result + MemoComponentsInfo + Newline + NewLine;
end;
if MemoGroupInfo <> '' then begin
Result := Result + MemoGroupInfo + Newline + NewLine;
end;
if MemoTasksInfo <> '' then begin
Result := Result + MemoTasksInfo + Newline + NewLine;
end;
end;
Function UninstallPrevious(const RootKey: Integer; const AppId,
Args: String): String;
var
UninstallString, msg: String;
ResultCode: Integer;
begin
If RegQueryStringValue(RootKey,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId,
'UninstallString', UninstallString) then
begin
UninstallString := RemoveQuotes(UninstallString);
Result := SetupMessage(msgCannotContinue);
msg := SetupMessage(msgConfirmuninstall);
StringChange(msg, '%1', '{#NAME}');
if (SuppressibleMsgBox(msg, mbConfirmation, MB_YESNO, IDYES)
= IDYES) and
Exec(UninstallString, Args, GetTempDir(), SW_HIDE,
ewWaitUntilTerminated, ResultCode) and
(ResultCode = 0)
then
SetLength(Result, 0);
end;
end;
{ Uninstall previous version before installing a new one. }
Function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
if WizardIsTaskSelected('stationary') then
begin
{ New Inno installer. }
Result := UninstallPrevious(HKA, '{#SetupSetting("AppId")}',
'/VERYSILENT');
{ Old NSIS installer. }
if Length(Result) = 0 then
Result := UninstallPrevious(HKLM, '{#NAME}', '/S');
end;
end;
Loading

0 comments on commit b272f54

Please sign in to comment.