diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 7f547bf..3b18d19 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -53,6 +53,8 @@ jobs: mingw-w64-clang-x86_64-bzip2 mingw-w64-clang-x86_64-zstd mingw-w64-clang-x86_64-xz + mingw-w64-clang-x86_64-imagemagick + mingw-w64-clang-x86_64-librsvg - name: Workspace Path Fixup run: | @@ -133,18 +135,39 @@ jobs: zip -r "nui-sftp-windows-x86_64_${{ env.VERSION }}.zip" "nui-sftp" mv "nui-sftp-windows-x86_64_${{ env.VERSION }}.zip" "${{ env.WSPACE }}" - - name: Produce Artifact + - name: Install Inno Setup + run: choco install innosetup -y --no-progress + + - name: Build installer + env: + SOURCE_DIR: ${{ env.WSPACE }}/nui-sftp/build/install + OUTPUT_DIR: ${{ env.WSPACE }}/installer-out + run: | + bash scripts/build_installer.sh \ + --version "${{ env.VERSION }}" \ + --source-dir "$SOURCE_DIR" \ + --output-dir "$OUTPUT_DIR" + mv "$OUTPUT_DIR/nui-sftp-windows-x86_64_${{ env.VERSION }}-setup.exe" "${{ env.WSPACE }}/" + + - name: Produce Zip Artifact uses: actions/upload-artifact@v6 with: name: nui-sftp-windows-x86_64_${{ env.VERSION }}.zip path: ${{ env.WSPACE }}/nui-sftp-windows-x86_64_${{ env.VERSION }}.zip - - name: Upload Artifact to Release + - name: Produce Installer Artifact + uses: actions/upload-artifact@v6 + with: + name: nui-sftp-windows-x86_64_${{ env.VERSION }}-setup.exe + path: ${{ env.WSPACE }}/nui-sftp-windows-x86_64_${{ env.VERSION }}-setup.exe + + - name: Upload Artifacts to Release if: github.ref == 'refs/heads/main' env: GH_TOKEN: ${{ secrets.RELEASEUPLOADPAT }} run: | gh release upload "v${{ env.VERSION }}" "${{ env.WSPACE }}/nui-sftp-windows-x86_64_${{ env.VERSION }}.zip" --repo 5cript/nui-sftp + gh release upload "v${{ env.VERSION }}" "${{ env.WSPACE }}/nui-sftp-windows-x86_64_${{ env.VERSION }}-setup.exe" --repo 5cript/nui-sftp - name: Upload artifacts to MEGA S4 if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' @@ -152,14 +175,18 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.MEGAS4ACCESS }} AWS_SECRET_ACCESS_KEY: ${{ secrets.MEGAS4KEY }} ENDPOINT: https://s3.eu-central-1.s4.mega.io - FILENAME: "nui-sftp-windows-x86_64_${{ env.VERSION }}.zip" + ZIP_FILENAME: "nui-sftp-windows-x86_64_${{ env.VERSION }}.zip" + SETUP_FILENAME: "nui-sftp-windows-x86_64_${{ env.VERSION }}-setup.exe" S3_PATH: "s3://nui-sftp-releases/windows/${{ (github.event_name == 'workflow_dispatch' && 'x86_64/user_trigger') || 'x86_64' }}/" run: | - # Check if the file exists on the remote - if aws s3 ls "${{ env.S3_PATH }}${{ env.FILENAME }}" --endpoint-url ${{ env.ENDPOINT }} > /dev/null 2>&1; then - echo "Error: File ${{ env.FILENAME }} already exists in S4. Aborting to prevent overwrite." - exit 1 - fi - - # If it doesnt exist, upload it - aws s3 cp "${{ env.WSPACE }}/${{ env.FILENAME }}" "${{ env.S3_PATH }}" --endpoint-url ${{ env.ENDPOINT }} \ No newline at end of file + upload_if_absent() { + local filename="$1" + if aws s3 ls "${{ env.S3_PATH }}${filename}" --endpoint-url "${{ env.ENDPOINT }}" > /dev/null 2>&1; then + echo "Error: File ${filename} already exists in S4. Aborting to prevent overwrite." + exit 1 + fi + aws s3 cp "${{ env.WSPACE }}/${filename}" "${{ env.S3_PATH }}" --endpoint-url "${{ env.ENDPOINT }}" + } + + upload_if_absent "${{ env.ZIP_FILENAME }}" + upload_if_absent "${{ env.SETUP_FILENAME }}" \ No newline at end of file diff --git a/PKGBUILD b/PKGBUILD index 6664036..ab2cba9 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Tim Ebbeke pkgname=nui-sftp -pkgver=1.1.1 +pkgver=1.2.0 pkgrel=1 pkgdesc="NUI-based SFTP application" arch=('x86_64') @@ -39,9 +39,9 @@ source=( "https://s3.g.s4.mega.io/jgemkib4a5fte35rktt5wxrwkw4ejk4ybemkf/nui-scp/icons.tar.gz" ) sha256sums=( - '9246f4da3d6b9f9d87fe57d2567e92f309f64a523604d89bf31edc794f597ee9' + '836af543956c5a2b632fe64d788fb160ed25f2c8bd85b338a04b56a16e80a825' 'cf0fc442069dfd28bd8b7769da6dfdca92111044ea9817197716ed9bfada3869' - '1a967af0ab93e5bac67e79b93b0272f7b79b79de7e4ff46b967d9c09e581969e' + '096a57b9d2ecaf3d37cdb7968cddecbf0daf96c180595d83577e9524421d7b68' '30ffa48c3a509e878db31a1e5d80376242852e34d9c2aa3b44d2e3d1da2ce32e' ) diff --git a/README.md b/README.md index 457d5a4..9048adb 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,37 @@ Submission-specific details that are still in flight (will be folded into - **AppImage** — `appimage/Dockerfile` is updated by `prepare_release.mjs`; the actual build runs in CI. +## Windows (Inno Setup) + +`windows/nui-sftp.iss` is an Inno Setup script that wraps the portable zip's +file tree into a per-machine installer with Start Menu / optional desktop / +optional PATH integration. Upgrades are handled automatically by Inno Setup's +stable `AppId` mechanism. No special migration logic is needed across +releases. + +`prepare_release.mjs` updates the `MyAppVersion` literal inside the `.iss` so +it stays in lockstep with `PKGBUILD`'s `pkgver` and the Flatpak manifest's +`FORCED_PROJECT_VERSION`. The CI workflow overrides it via ISCC's `/D` flag at +build time; the literal only matters for ad-hoc local invocations. + +### Local build + +The local build expects the [main scp repo](https://github.com/5cript/nui-sftp) +to be checked out as a sibling of this repo (i.e. `../nui-sftp/`). Inside the +scp checkout, run a Windows build (MSYS2 clang64, see scp's +`.github/workflows/windows.yml`) and then `bash scripts/deploy.sh` to stage +the install tree at `../nui-sftp/build/install`. + +Then, from this repo: + +```bash +./scripts/build_installer.sh --version 1.2.3 +``` + +The script auto-locates ISCC.exe (Inno Setup 6) and writes the installer to +`build/nui-sftp-windows-x86_64_-setup.exe`. Install Inno Setup with +`winget install JRSoftware.InnoSetup` if missing. + ## Local Flatpak builds [`scripts/build_flatpak.sh`](scripts/build_flatpak.sh) invokes diff --git a/arch b/arch index e4e5f34..9cbc911 160000 --- a/arch +++ b/arch @@ -1 +1 @@ -Subproject commit e4e5f34bb39d3ff0c6da67c0210d62de46031bae +Subproject commit 9cbc911640b7e5d849db64eed1c52da94f70b787 diff --git a/flathub b/flathub index 5709989..a1fc4d0 160000 --- a/flathub +++ b/flathub @@ -1 +1 @@ -Subproject commit 5709989e7a7dc2691677d11c25d8689ecf68a8b4 +Subproject commit a1fc4d074aa3991507aec8d1005c42853f1f7e13 diff --git a/org.nuicpp.nui_sftp.metainfo.xml b/org.nuicpp.nui_sftp.metainfo.xml index b13a60f..38b67f9 100644 --- a/org.nuicpp.nui_sftp.metainfo.xml +++ b/org.nuicpp.nui_sftp.metainfo.xml @@ -36,6 +36,19 @@ + + +

What's Changed

+
    +
  • Add ico for windows as app icon. by @5cript in https://github.com/5cript/nui-sftp/pull/170
  • +
+

+ + Full Changelog + : https://github.com/5cript/nui-sftp/compare/v1.1.1
v1.2.0 +

+
+

What's Changed

diff --git a/org.nuicpp.nui_sftp.yml b/org.nuicpp.nui_sftp.yml index e595c15..f719f19 100644 --- a/org.nuicpp.nui_sftp.yml +++ b/org.nuicpp.nui_sftp.yml @@ -70,8 +70,8 @@ modules: - cp -r * /app/frontend sources: - type: archive - url: https://github.com/5cript/nui-sftp/releases/download/v1.1.1/nui-sftp-linux-frontend_1.1.1.tar.gz - sha256: 1a967af0ab93e5bac67e79b93b0272f7b79b79de7e4ff46b967d9c09e581969e + url: https://github.com/5cript/nui-sftp/releases/download/v1.2.0/nui-sftp-linux-frontend_1.2.0.tar.gz + sha256: 096a57b9d2ecaf3d37cdb7968cddecbf0daf96c180595d83577e9524421d7b68 - name: nui-sftp buildsystem: cmake-ninja @@ -116,8 +116,8 @@ modules: sources: - type: git url: https://github.com/5cript/nui-sftp - tag: v1.1.1 - commit: 2e78074b4fd5a92a38b9901081c3e464c08b7238 + tag: v1.2.0 + commit: ef23dde8e0fc5bee03797142121a06afc493c9fc - type: archive url: https://s3.g.s4.mega.io/jgemkib4a5fte35rktt5wxrwkw4ejk4ybemkf/nui-scp/icons.tar.gz sha256: 30ffa48c3a509e878db31a1e5d80376242852e34d9c2aa3b44d2e3d1da2ce32e diff --git a/scripts/build_installer.sh b/scripts/build_installer.sh new file mode 100644 index 0000000..684ea2b --- /dev/null +++ b/scripts/build_installer.sh @@ -0,0 +1,213 @@ +#!/bin/bash + +# Build the nui-sftp Windows installer locally or in CI. +# +# Assumes deploy.sh from the main scp repo has already been run, producing the +# staged install tree (default: ../nui-sftp/build/install relative to this repo). +# Invokes ISCC against windows/nui-sftp.iss with the staged tree as SourceDir. + +set -e +set -u + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/lib.sh" + +REPO_DIR=$(canonicalPath "${SCRIPT_DIR}/..") +ISS_FILE="${REPO_DIR}/windows/nui-sftp.iss" + +VERSION="" +SOURCE_DIR_ARG="" +ICON_FILE_ARG="" +OUTPUT_DIR_ARG="${REPO_DIR}/build" + +usage() { + cat < Version string baked into the installer + (default: read from PKGBUILD via extract_version.sh) + --source-dir Path to the staged install tree from deploy.sh + (default: ../nui-sftp/build/install relative to this repo) + --icon Path to nui-sftp.ico for SetupIconFile + (default: /assets/icons/nui-sftp.ico) + --output-dir Where the installer .exe is written + (default: ./build) + -h | --help Show this help + +Environment overrides (lowercase wins over flag): + ISCC Path to ISCC.exe (otherwise auto-located) + +Example: + $0 --version 1.2.3 +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --source-dir) SOURCE_DIR_ARG="$2"; shift 2 ;; + --icon) ICON_FILE_ARG="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR_ARG="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage; exit 1 ;; + esac +done + +if [ -z "${VERSION}" ]; then + if [ -x "${SCRIPT_DIR}/extract_version.sh" ]; then + VERSION=$("${SCRIPT_DIR}/extract_version.sh") + else + echo "Error: --version not supplied and extract_version.sh not found" >&2 + exit 1 + fi +fi +VERSION="${VERSION#v}" + +SOURCE_DIR="${SOURCE_DIR_ARG:-${REPO_DIR}/../nui-sftp/build/install}" +SOURCE_DIR=$(canonicalPath "${SOURCE_DIR}") + +ICON_FILE="${ICON_FILE_ARG:-${SOURCE_DIR}/assets/icons/nui-sftp.ico}" + +mkdir -p "${OUTPUT_DIR_ARG}" +OUTPUT_DIR=$(canonicalPath "${OUTPUT_DIR_ARG}") + +if [ ! -d "${SOURCE_DIR}" ]; then + echo "Error: source directory does not exist: ${SOURCE_DIR}" >&2 + echo "Hint: run 'bash scripts/deploy.sh' in the main scp repo first." >&2 + exit 1 +fi +if [ ! -f "${SOURCE_DIR}/bin/nui-sftp.exe" ]; then + echo "Error: ${SOURCE_DIR}/bin/nui-sftp.exe not found." >&2 + echo "Hint: the source dir must be a Windows deploy.sh output, not a Linux one." >&2 + exit 1 +fi +if [ ! -f "${ICON_FILE}" ]; then + echo "Error: icon file not found: ${ICON_FILE}" >&2 + echo "Hint: build the main scp repo with ImageMagick installed so the .ico is generated." >&2 + exit 1 +fi + +# Convert a Windows-style path (C:\foo\bar) to bash form (/c/foo/bar). +win_to_unix() { + local p="$1" + if command -v cygpath > /dev/null 2>&1; then + cygpath -u "$p" + return + fi + p="${p//\\//}" + if [[ "$p" =~ ^([A-Za-z]):(/.*)?$ ]]; then + local drive="${BASH_REMATCH[1]}" + # lowercase drive letter + drive="$(echo "$drive" | tr '[:upper:]' '[:lower:]')" + echo "/${drive}${BASH_REMATCH[2]}" + else + echo "$p" + fi +} + +# Locate ISCC.exe +locate_iscc() { + if [ -n "${ISCC:-}" ] && [ -x "${ISCC}" ]; then + echo "${ISCC}" + return + fi + local candidates=( + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" + "/c/Program Files/Inno Setup 6/ISCC.exe" + ) + # winget per-user install — $HOME differs between git-bash and MSYS2, + # so prefer the Windows env vars and convert them. + if [ -n "${LOCALAPPDATA:-}" ]; then + candidates+=("$(win_to_unix "${LOCALAPPDATA}")/Programs/Inno Setup 6/ISCC.exe") + fi + if [ -n "${USERPROFILE:-}" ]; then + candidates+=("$(win_to_unix "${USERPROFILE}")/AppData/Local/Programs/Inno Setup 6/ISCC.exe") + fi + candidates+=("${HOME}/AppData/Local/Programs/Inno Setup 6/ISCC.exe") + for c in "${candidates[@]}"; do + if [ -x "${c}" ]; then + echo "${c}" + return + fi + done + # Fall back to the uninstall registry — handles future version dirs + # and non-default install locations. + if command -v reg.exe > /dev/null 2>&1; then + local key path_win path_unix + for key in \ + 'HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1' \ + 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1' \ + 'HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1'; do + path_win=$(reg.exe query "${key}" /v InstallLocation 2>/dev/null \ + | grep -oE '[A-Za-z]:\\.*' | head -1 | tr -d '\r') + if [ -n "${path_win}" ]; then + if command -v cygpath > /dev/null 2>&1; then + path_unix=$(cygpath -u "${path_win}") + else + path_unix="${path_win//\\//}" + fi + path_unix="${path_unix%/}/ISCC.exe" + if [ -x "${path_unix}" ]; then + echo "${path_unix}" + return + fi + fi + done + fi + if command -v ISCC.exe > /dev/null 2>&1; then + command -v ISCC.exe + return + fi + if command -v iscc > /dev/null 2>&1; then + command -v iscc + return + fi + return 1 +} + +ISCC_PATH=$(locate_iscc || true) +if [ -z "${ISCC_PATH}" ]; then + echo "Error: ISCC.exe not found." >&2 + echo "Install Inno Setup: winget install JRSoftware.InnoSetup" >&2 + echo "Checked: \$ISCC, \$HOME/AppData/Local/Programs/Inno Setup 6 (winget per-user)," >&2 + echo " Program Files [(x86)], Uninstall registry, and \$PATH." >&2 + echo "Or set ISCC=/path/to/ISCC.exe and re-run." >&2 + exit 1 +fi + +echo "ISCC: ${ISCC_PATH}" +echo "VERSION: ${VERSION}" +echo "SourceDir: ${SOURCE_DIR}" +echo "IconFile: ${ICON_FILE}" +echo "OutputDir: ${OUTPUT_DIR}" + +# ISCC wants Windows-style paths under MSYS2; cygpath handles the conversion. +to_win() { + if command -v cygpath > /dev/null 2>&1; then + cygpath -w "$1" + else + echo "$1" + fi +} + +# MSYS2/Cygwin mangle args that start with '/' into Windows paths before +# passing them to native exes. Disable that for the ISCC call so /Qp and /D +# switches survive intact. +MSYS2_ARG_CONV_EXCL='*' MSYS_NO_PATHCONV=1 \ +"${ISCC_PATH}" \ + "/Qp" \ + "/DMyAppVersion=${VERSION}" \ + "/DSourceDir=$(to_win "${SOURCE_DIR}")" \ + "/DIconFile=$(to_win "${ICON_FILE}")" \ + "/DOutputDir=$(to_win "${OUTPUT_DIR}")" \ + "$(to_win "${ISS_FILE}")" + +INSTALLER="${OUTPUT_DIR}/nui-sftp-windows-x86_64_${VERSION}-setup.exe" +if [ -f "${INSTALLER}" ]; then + echo + echo "Installer produced: ${INSTALLER}" +else + echo "Error: ISCC reported success but ${INSTALLER} is missing." >&2 + exit 1 +fi diff --git a/scripts/prepare_release.mjs b/scripts/prepare_release.mjs index 10b6e4c..a0e1c27 100755 --- a/scripts/prepare_release.mjs +++ b/scripts/prepare_release.mjs @@ -1,4 +1,4 @@ -#!/bin/bun +#!/bin/env bun import { updatePkgBuild } from "./update_scripts/pkgbuild.mjs"; import { updateRepo } from "./update_scripts/git.mjs"; @@ -6,6 +6,7 @@ import { nuiSftpRepoDir } from "./update_scripts/files_and_dirs.mjs"; import { updateReleasesInMetainfoXml, parseMetainfoXml } from "./update_scripts/metainfo.mjs"; import { updateFlatpakYaml } from "./update_scripts/flatpak.mjs"; import { updateAppImageDockerfile } from "./update_scripts/appimage.mjs"; +import { updateInnoSetup } from "./update_scripts/innosetup.mjs"; import { version } from "./update_scripts/args.mjs"; const repoUrl = 'https://github.com/5cript/nui-sftp.git'; @@ -32,3 +33,8 @@ await updateAppImageDockerfile().catch((err) => { console.error('Error updating AppImage Dockerfile:', err); process.exit(1); }); + +await updateInnoSetup().catch((err) => { + console.error('Error updating Inno Setup script:', err); + process.exit(1); +}); diff --git a/scripts/update_scripts/files_and_dirs.mjs b/scripts/update_scripts/files_and_dirs.mjs index 30d986c..b7b4907 100644 --- a/scripts/update_scripts/files_and_dirs.mjs +++ b/scripts/update_scripts/files_and_dirs.mjs @@ -7,5 +7,6 @@ const cloneLocation = path.join(sourceDir, 'checkout'); const nuiSftpRepoDir = `${cloneLocation}/nui-sftp`; const flatpakYamlPath = path.join(sourceDir, 'org.nuicpp.nui_sftp.yml'); const appimageDockerfilePath = path.join(sourceDir, 'appimage', 'Dockerfile'); +const innoSetupIssPath = path.join(sourceDir, 'windows', 'nui-sftp.iss'); -export { pkgbuildPath, sourceDir, nuiSftpRepoDir, flatpakYamlPath, appimageDockerfilePath }; \ No newline at end of file +export { pkgbuildPath, sourceDir, nuiSftpRepoDir, flatpakYamlPath, appimageDockerfilePath, innoSetupIssPath }; \ No newline at end of file diff --git a/scripts/update_scripts/innosetup.mjs b/scripts/update_scripts/innosetup.mjs new file mode 100644 index 0000000..f8c7999 --- /dev/null +++ b/scripts/update_scripts/innosetup.mjs @@ -0,0 +1,31 @@ +import fs from 'node:fs/promises'; +import { parsedVersion } from './version.mjs'; +import { innoSetupIssPath } from './files_and_dirs.mjs'; +import { splitLines, onLineMatchingModify, findLineIndexMatching } from './text_editing.mjs'; + +// Matches `#define MyAppVersion "1.2.3"` (with or without surrounding whitespace). +const versionLineRegex = /^(\s*#define\s+MyAppVersion\s+)"([^"]*)"\s*$/; + +const updateInnoSetup = async () => { + console.log(`Updating Inno Setup script at ${innoSetupIssPath}...`); + + const version = parsedVersion(); + let lines = splitLines(await fs.readFile(innoSetupIssPath, 'utf-8')); + + if (findLineIndexMatching(lines, versionLineRegex).lineIndex === -1) { + throw new Error( + `No '#define MyAppVersion "..."' line found in ${innoSetupIssPath}. ` + + `Has the .iss been hand-edited?` + ); + } + + lines = onLineMatchingModify(lines, versionLineRegex, (_line, match) => { + console.log(`Updating MyAppVersion from ${match[2]} to ${version.full}...`); + return `${match[1]}"${version.full}"`; + }); + + await fs.writeFile(innoSetupIssPath, lines.join('\n'), 'utf-8'); + console.log('Inno Setup script updated successfully.'); +}; + +export { updateInnoSetup }; diff --git a/scripts/update_scripts/pkgbuild.mjs b/scripts/update_scripts/pkgbuild.mjs index d9e164e..1b1c66c 100644 --- a/scripts/update_scripts/pkgbuild.mjs +++ b/scripts/update_scripts/pkgbuild.mjs @@ -1,44 +1,29 @@ import { spawn } from 'node:child_process'; import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { createHash } from 'node:crypto'; import { parsedVersion } from './version.mjs'; import { pkgbuildPath, nuiSftpRepoDir } from './files_and_dirs.mjs'; import { splitLines, onLineMatchingModify, findLineIndexMatching } from './text_editing.mjs'; import fs from 'node:fs/promises'; -import fsOld from 'node:fs'; -import path from 'node:path'; -import os from 'node:os'; function calcChecksumGitTag(checkoutDir, tag) { return new Promise((resolve, reject) => { - const tmpFile = path.join(os.tmpdir(), `git-archive-${process.pid}-${Date.now()}.tar`); - const out = fsOld.createWriteStream(tmpFile); - const git = spawn("git", [ "-c", "core.abbrev=no", "-C", checkoutDir, "archive", "--format=tar", tag ]); - git.stdout.pipe(out); + const hash = createHash("sha256"); + git.stdout.pipe(hash); git.stderr.pipe(process.stderr); git.on("error", reject); git.on("close", (code) => { - if (code !== 0) { - fs.unlink(tmpFile, () => { }); - return reject(new Error(`git archive exited with code ${code}`)); - } - - const hash = spawn("sha256sum", [tmpFile]); - let output = ""; - hash.stdout.on("data", (d) => { output += d.toString("utf8"); }); - hash.stderr.pipe(process.stderr); - hash.on("close", (hashCode) => { - fs.unlink(tmpFile, () => { }); - if (hashCode !== 0) return reject(new Error(`sha256sum exited with code ${hashCode}`)); - resolve(output.trim().split(/\s+/)[0]); - }); + if (code !== 0) return reject(new Error(`git archive exited with code ${code}`)); + resolve(hash.digest("hex")); }); }); } @@ -47,18 +32,9 @@ async function calcChecksumHttp(url) { const res = await fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); - const hash = spawn("sha256sum", [], { stdio: ['pipe', 'pipe', 'inherit'] }); - Readable.fromWeb(res.body).pipe(hash.stdin); - - let output = ""; - hash.stdout.on("data", (d) => { output += d.toString("utf8"); }); - - return new Promise((resolve, reject) => { - hash.on("close", (code) => { - if (code !== 0) return reject(new Error(`sha256sum exited with code ${code}`)); - resolve(output.trim().split(/\s+/)[0]); - }); - }); + const hash = createHash("sha256"); + await pipeline(Readable.fromWeb(res.body), hash); + return hash.digest("hex"); } const findSha256sumsRange = (lines) => { diff --git a/windows/modpath.iss b/windows/modpath.iss new file mode 100644 index 0000000..82ea09b --- /dev/null +++ b/windows/modpath.iss @@ -0,0 +1,170 @@ +(* + modpath.iss - minimal PATH editor for Inno Setup. + + This file is #included from inside a [Code] section of the host .iss, + so the file-level documentation must use Pascal-style block comments + rather than ';' section comments. + + Host .iss must define: + ModPathName - the [Tasks] name guarding PATH modification (e.g. 'modifypath') + ModPathType - 'system' (HKLM, machine-wide) or 'user' (HKCU) + and implement: + function ModPathDir(): TArrayOfString; - directories to add/remove + + On install: appends each ModPathDir() entry that isn't already on PATH. + On uninstall: removes each ModPathDir() entry from PATH. + Broadcasts WM_SETTINGCHANGE so new shells / Explorer pick up the change + without requiring a reboot. +*) + +#ifndef ModPathName + #error ModPathName must be defined before including modpath.iss +#endif +#ifndef ModPathType + #error ModPathType must be defined ('system' or 'user') before including modpath.iss +#endif + +[Code] +const + ModPath_SystemKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; + ModPath_UserKey = 'Environment'; + ModPath_WM_SETTINGCHANGE = $001A; + ModPath_HWND_BROADCAST = $FFFF; + ModPath_SMTO_ABORTIFHUNG = $0002; + +function ModPath_SendMessageTimeout( + hWnd: HWND; Msg: UINT; wParam: Longint; lParam: PAnsiChar; + fuFlags: UINT; uTimeout: UINT; var lpdwResult: Longint): Longint; + external 'SendMessageTimeoutA@user32.dll stdcall'; + +function ModPath_IsSystem(): Boolean; +begin + Result := CompareText('{#ModPathType}', 'system') = 0; +end; + +function ModPath_RootKey(): Integer; +begin + if ModPath_IsSystem() then + Result := HKEY_LOCAL_MACHINE + else + Result := HKEY_CURRENT_USER; +end; + +function ModPath_SubKey(): String; +begin + if ModPath_IsSystem() then + Result := ModPath_SystemKey + else + Result := ModPath_UserKey; +end; + +function ModPath_ReadPath(): String; +begin + if not RegQueryStringValue(ModPath_RootKey(), ModPath_SubKey(), 'Path', Result) then + Result := ''; +end; + +procedure ModPath_WritePath(const Value: String); +var + Dummy: Longint; +begin + RegWriteExpandStringValue(ModPath_RootKey(), ModPath_SubKey(), 'Path', Value); + ModPath_SendMessageTimeout(ModPath_HWND_BROADCAST, ModPath_WM_SETTINGCHANGE, + 0, 'Environment', ModPath_SMTO_ABORTIFHUNG, 5000, Dummy); +end; + +function ModPath_Split(const S, Sep: String): TArrayOfString; +var + Cursor, Found, Count: Integer; + Rest: String; +begin + SetArrayLength(Result, 0); + Rest := S; + Count := 0; + repeat + Found := Pos(Sep, Rest); + SetArrayLength(Result, Count + 1); + if Found > 0 then begin + Result[Count] := Copy(Rest, 1, Found - 1); + Rest := Copy(Rest, Found + Length(Sep), MaxInt); + end else begin + Result[Count] := Rest; + Rest := ''; + end; + Inc(Count); + Cursor := Found; + until Cursor = 0; +end; + +function ModPath_Contains(const Path, Entry: String): Boolean; +var + Parts: TArrayOfString; + I: Integer; +begin + Result := False; + Parts := ModPath_Split(Path, ';'); + for I := 0 to GetArrayLength(Parts) - 1 do + if CompareText(Trim(Parts[I]), Trim(Entry)) = 0 then begin + Result := True; + Exit; + end; +end; + +procedure ModPath_Add(const Entry: String); +var + Path: String; +begin + Path := ModPath_ReadPath(); + if ModPath_Contains(Path, Entry) then + Exit; + if (Path <> '') and (Copy(Path, Length(Path), 1) <> ';') then + Path := Path + ';'; + Path := Path + Entry; + ModPath_WritePath(Path); +end; + +procedure ModPath_Remove(const Entry: String); +var + Path, NewPath: String; + Parts: TArrayOfString; + I: Integer; +begin + Path := ModPath_ReadPath(); + if Path = '' then + Exit; + Parts := ModPath_Split(Path, ';'); + NewPath := ''; + for I := 0 to GetArrayLength(Parts) - 1 do begin + if CompareText(Trim(Parts[I]), Trim(Entry)) = 0 then + Continue; + if NewPath = '' then + NewPath := Parts[I] + else + NewPath := NewPath + ';' + Parts[I]; + end; + if NewPath <> Path then + ModPath_WritePath(NewPath); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + Dirs: TArrayOfString; + I: Integer; +begin + if (CurStep <> ssPostInstall) then Exit; + if not WizardIsTaskSelected('{#ModPathName}') then Exit; + Dirs := ModPathDir(); + for I := 0 to GetArrayLength(Dirs) - 1 do + ModPath_Add(Dirs[I]); +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + Dirs: TArrayOfString; + I: Integer; +begin + if (CurUninstallStep <> usPostUninstall) then Exit; + Dirs := ModPathDir(); + for I := 0 to GetArrayLength(Dirs) - 1 do + ModPath_Remove(Dirs[I]); +end; diff --git a/windows/nui-sftp.iss b/windows/nui-sftp.iss new file mode 100644 index 0000000..a0ee90b --- /dev/null +++ b/windows/nui-sftp.iss @@ -0,0 +1,101 @@ +; ----------------------------------------------------------------------------- +; nui-sftp Inno Setup installer +; +; Build locally with scripts/build_installer.sh (see README.md). CI calls the +; same script from .github/workflows/windows.yml. Do not invoke ISCC directly +; against this file unless you know what you're doing -- the script sets up +; SourceDir/IconFile/OutputDir/MyAppVersion via /D flags. +; +; AppId MUST stay stable across releases. Same GUID = Inno Setup recognises an +; existing install and does an in-place upgrade. Regenerating the GUID would +; orphan every prior install. +; ----------------------------------------------------------------------------- + +; MyAppVersion is rewritten by scripts/update_scripts/innosetup.mjs on every +; `prepare_release.mjs --version X.Y.Z` invocation. Do not edit by hand; bump +; via prepare_release.mjs so it stays in lockstep with PKGBUILD/flatpak/appimage. +#ifndef MyAppVersion + #define MyAppVersion "1.2.0" +#endif +#ifndef SourceDir + #define SourceDir "..\..\nui-sftp\build\install" +#endif +#ifndef OutputDir + #define OutputDir "..\build" +#endif +#ifndef IconFile + #define IconFile SourceDir + "\assets\icons\nui-sftp.ico" +#endif + +#define MyAppName "nui-sftp" +#define MyAppPublisher "Tim Ebbeke" +#define MyAppURL "https://github.com/5cript/nui-sftp" +#define MyAppExeName "nui-sftp.exe" + +[Setup] +AppId={{980375E8-DDF7-443B-8557-8F649B062FE3} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL}/issues +AppUpdatesURL={#MyAppURL}/releases +VersionInfoVersion={#MyAppVersion} +VersionInfoProductName={#MyAppName} +VersionInfoCompany={#MyAppPublisher} + +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +DisableDirPage=no + +PrivilegesRequired=admin +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible + +Compression=lzma2/ultra +SolidCompression=yes +WizardStyle=modern + +SetupIconFile={#IconFile} +UninstallDisplayIcon={app}\bin\{#MyAppExeName} +UninstallDisplayName={#MyAppName} {#MyAppVersion} + +OutputDir={#OutputDir} +OutputBaseFilename=nui-sftp-windows-x86_64_{#MyAppVersion}-setup + +CloseApplications=yes +RestartApplications=no + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked +Name: "modifypath"; Description: "Add nui-sftp to the system PATH"; GroupDescription: "System integration:"; Flags: unchecked + +[Files] +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +; Shortcuts intentionally omit IconFilename so Windows picks up the icon +; resource embedded in nui-sftp.exe (added by the main repo's CMake .rc step). +Name: "{group}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}" +Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\bin\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent + +#define ModPathName 'modifypath' +#define ModPathType 'system' + +[Code] +function ModPathDir(): TArrayOfString; +begin + SetArrayLength(Result, 1); + Result[0] := ExpandConstant('{app}\bin'); +end; + +#include "modpath.iss"