Skip to content
Merged
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
17 changes: 12 additions & 5 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# All text files use LF so Spotless checks and checksum-based tests are stable
# on Windows runners (where core.autocrlf would otherwise convert to CRLF).
* text=auto eol=lf

# These are Windows script files and should use crlf
# Windows script files keep CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf

# Binary files should be left untouched
# Binary files should be left untouched.
*.jar binary

*.png binary
*.ico binary
*.icns binary
*.pdf binary
*.xlsx binary
*.zip binary
2 changes: 1 addition & 1 deletion .github/workflows/build-distributions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- os: macos-latest
platform: macos
tasks: packageMacosAppImage
artifact: app/build/release/macos/*
artifact: app/build/release/macos/AlipsaAccounting-macos.zip

runs-on: ${{ matrix.os }}

Expand Down
36 changes: 34 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def licenseFilePath = rootProject.file('LICENSE').absolutePath

def linuxDesktopTemplate = releaseConfig.packagingRoot.dir('linux/desktop').file("${releaseConfig.appName}.desktop.template")
def linuxDesktopEntry = releaseConfig.generatedRoot.map { it.file("linux-desktop/${releaseConfig.appName}.desktop") }
def linuxInstallTemplate = releaseConfig.packagingRoot.dir('linux').file('install.sh.template')
def linuxUninstallTemplate = releaseConfig.packagingRoot.dir('linux').file('uninstall.sh.template')
def linuxInstallScript = releaseConfig.generatedRoot.map { it.file('linux-scripts/install.sh') }
def linuxUninstallScript = releaseConfig.generatedRoot.map { it.file('linux-scripts/uninstall.sh') }

def currentOsName = System.getProperty('os.name', 'unknown').toLowerCase(Locale.ROOT)
def currentPlatform = currentOsName.contains('win') ? 'windows' : currentOsName.contains('mac') ? 'macos' : 'linux'
Expand Down Expand Up @@ -186,6 +190,26 @@ tasks.register('generateLinuxDesktopEntry') {
}
}

tasks.register('generateLinuxInstallScripts') {
inputs.file(linuxInstallTemplate)
inputs.file(linuxUninstallTemplate)
outputs.file(linuxInstallScript)
outputs.file(linuxUninstallScript)
doLast {
[
(linuxInstallTemplate.asFile): linuxInstallScript.get().asFile,
(linuxUninstallTemplate.asFile): linuxUninstallScript.get().asFile
].each { File source, File target ->
target.parentFile.mkdirs()
target.text = source.getText('UTF-8')
.replace('@DISPLAY_NAME@', releaseConfig.displayName)
.replace('@DESCRIPTION@', releaseConfig.description)
.replace('@APP_NAME@', releaseConfig.appName)
.replace('@PACKAGE_NAME@', releaseConfig.packageName)
}
}
}

tasks.register('prepareLinuxPackagingResources', Copy) {
dependsOn 'generateReleaseMetadata', 'generateLinuxDesktopEntry'
from(releaseConfig.packagingRoot.dir('linux'))
Expand Down Expand Up @@ -242,15 +266,23 @@ tasks.register('packageLinuxAppImage', Exec) {

tasks.register('packageLinuxReleaseZip', Zip) {
onlyIf { isLinux }
dependsOn 'packageLinuxAppImage', 'generateLinuxDesktopEntry'
dependsOn 'packageLinuxAppImage', 'generateLinuxInstallScripts'
archiveBaseName = releaseConfig.packageName
archiveVersion = releaseConfig.version
archiveClassifier = 'linux'
destinationDirectory = releaseConfig.releaseRoot.map { it.dir('linux') }
from(releaseConfig.releaseRoot.map { it.dir("linux/${releaseConfig.appName}") }) {
into(releaseConfig.appName)
filesMatching("bin/${releaseConfig.appName}") {
it.permissions { unix(0755) }
}
}
from(linuxInstallScript) {
filePermissions { unix(0755) }
}
from(linuxUninstallScript) {
filePermissions { unix(0755) }
}
from(linuxDesktopEntry)
}

tasks.register('packageWindowsAppImage', Exec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ final class AlipsaAccounting {
log.warning("Launch verification completed with warnings: ${startupReport.warnings.join(' | ')}")
}
System.out.println("Launch verification OK: ${versionLine()} [home=${AppPaths.applicationHome()}]")
// Release the H2 and logging file handles so the caller (tests, packaging verification)
// can delete the verification home directory on Windows.
DatabaseService.instance.shutdown()
LoggingConfigurer.shutdown()
return
}
if (!startupReport.ok || !startupReport.warnings.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ final class DatabaseService {
}
}

/**
* Issues an H2 {@code SHUTDOWN} so the engine releases its file handles. Required on Windows
* before deleting the database directory (e.g. JUnit {@code @TempDir} cleanup), since H2 keeps
* the {@code .mv.db} file open between connections.
*/
void shutdown() {
Sql sql = Sql.newInstance(databaseUrl(), USERNAME, PASSWORD, DRIVER)
try {
sql.execute('shutdown')
} catch (java.sql.SQLException ignored) {
// H2 closes the connection as part of SHUTDOWN and reports "Database is already closed"
// when the Statement tries to finalize. The shutdown itself succeeded.
} finally {
try {
sql.close()
} catch (java.sql.SQLException ignored) {
// Connection is already closed by SHUTDOWN.
}
}
}

private String defaultDatabaseUrl() {
embeddedDatabaseUrl(AppPaths.applicationHome())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ class ReportServicesTest {
assertEquals(ReportType.INCOME_STATEMENT, archive.reportType)
assertTrue(new String(pdf, 0, 5, StandardCharsets.US_ASCII).startsWith('%PDF-'))
assertTrue(Files.size(reportArchiveService.resolveStoredPath(archive)) > 500L)
assertEquals('ccf4d53a601b18ee472bdb2c9b945f7b8bb1c297239e58cb3f7bc2ebbf56e19d', sha256(journoReportService.renderHtml(preview).getBytes(StandardCharsets.UTF_8)))
String html = journoReportService.renderHtml(preview).replace('\r\n', '\n').replace('\r', '\n')
assertEquals('ccf4d53a601b18ee472bdb2c9b945f7b8bb1c297239e58cb3f7bc2ebbf56e19d', sha256(html.getBytes(StandardCharsets.UTF_8)))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import java.nio.file.Path
import java.nio.file.Paths
Expand All @@ -14,6 +15,9 @@ import java.nio.file.Paths
// interfering with other tests that call AppPaths.applicationHome() concurrently.
class AppPathsTest {

@TempDir
Path tempDir

private String previousOsName
private String previousUserHome
private String previousHomeOverride
Expand Down Expand Up @@ -59,11 +63,12 @@ class AppPathsTest {

@Test
void applicationHomeOverrideTakesPrecedenceForAllDerivedDirectories() {
System.setProperty(AppPaths.HOME_OVERRIDE_PROPERTY, '/tmp/accounting-home')
Path overrideHome = tempDir.resolve('accounting-home')
System.setProperty(AppPaths.HOME_OVERRIDE_PROPERTY, overrideHome.toString())

Path home = AppPaths.applicationHome()

assertEquals(Paths.get('/tmp/accounting-home'), home)
assertEquals(overrideHome.toAbsolutePath().normalize(), home)
assertEquals(home.resolve('data'), AppPaths.dataDirectory())
assertEquals(home.resolve('logs'), AppPaths.logDirectory())
assertEquals(home.resolve('attachments'), AppPaths.attachmentsDirectory())
Expand Down
Empty file modified createDist.sh
100644 → 100755
Empty file.
59 changes: 59 additions & 0 deletions packaging/linux/install.sh.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
#
# Installationsskript för @DISPLAY_NAME@.
# Registrerar en desktop-genväg mot den plats där zip:en har extraherats,
# och säkerställer att launchern är exekverbar.
#
# Användning: kör ./install.sh från samma katalog som @APP_NAME@/

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_DIR="${SCRIPT_DIR}/@APP_NAME@"

if [ ! -d "${INSTALL_DIR}" ]; then
echo "Fel: hittade inte ${INSTALL_DIR}." >&2
echo "Kör install.sh från samma katalog som @APP_NAME@/." >&2
exit 1
fi

LAUNCHER="${INSTALL_DIR}/bin/@APP_NAME@"
if [ ! -f "${LAUNCHER}" ]; then
echo "Fel: launchern saknas: ${LAUNCHER}" >&2
exit 1
fi
chmod +x "${LAUNCHER}"

ICON="${INSTALL_DIR}/lib/@APP_NAME@.png"
if [ ! -f "${ICON}" ]; then
ICON="${SCRIPT_DIR}/@APP_NAME@.png"
fi

APPLICATIONS_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
mkdir -p "${APPLICATIONS_DIR}"

DESKTOP_FILE="${APPLICATIONS_DIR}/@APP_NAME@.desktop"
cat > "${DESKTOP_FILE}" <<DESKTOP_EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=@DISPLAY_NAME@
Comment=@DESCRIPTION@
Exec=${LAUNCHER}
Icon=${ICON}
Terminal=false
Categories=Office;Finance;
StartupWMClass=@APP_NAME@
DESKTOP_EOF

chmod 644 "${DESKTOP_FILE}"

if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "${APPLICATIONS_DIR}" >/dev/null 2>&1 || true
fi

echo "Installerat."
echo " Launcher: ${LAUNCHER}"
echo " Genväg: ${DESKTOP_FILE}"
echo
echo "Starta från applikationsmenyn, eller direkt via launchern ovan."
22 changes: 22 additions & 0 deletions packaging/linux/uninstall.sh.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
#
# Avinstallationsskript för @DISPLAY_NAME@.
# Tar bort desktop-genvägen. Radera sedan katalogen @APP_NAME@/ manuellt.

set -euo pipefail

APPLICATIONS_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
DESKTOP_FILE="${APPLICATIONS_DIR}/@APP_NAME@.desktop"

if [ -f "${DESKTOP_FILE}" ]; then
rm -f "${DESKTOP_FILE}"
echo "Desktop-genvägen borttagen: ${DESKTOP_FILE}"
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "${APPLICATIONS_DIR}" >/dev/null 2>&1 || true
fi
else
echo "Ingen genväg att ta bort (${DESKTOP_FILE} finns inte)."
fi

echo "Radera katalogen @APP_NAME@/ manuellt om du vill ta bort applikationen helt."
echo "Användardata ligger under \${XDG_DATA_HOME:-\$HOME/.local/share}/alipsa-accounting — ta bort det separat om det inte ska sparas."
25 changes: 19 additions & 6 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,24 @@ if [ "$BUILD" = true ]; then

echo "Downloading artifacts to $DIST_DIR/"
rm -rf "$DIST_DIR"
mkdir -p "$DIST_DIR"
gh run download "$run_id" --repo "$REPO" --dir "$DIST_DIR"
mkdir -p "$DIST_DIR/extras"
gh run download "$run_id" --repo "$REPO" --dir "$DIST_DIR/extras"

# Promote known distribution files to the dist root; leave anything else
# behind in extras/ so the root stays clean.
shopt -s nullglob
for f in "$DIST_DIR"/extras/*/alipsa-accounting-*.zip \
"$DIST_DIR"/extras/*/AlipsaAccounting-*.exe \
"$DIST_DIR"/extras/*/AlipsaAccounting-*.zip \
"$DIST_DIR"/extras/*/app-*.zip; do
mv "$f" "$DIST_DIR/"
done
shopt -u nullglob

# Flatten: move files out of per-platform subdirectories (no-clobber to avoid overwrites)
find "$DIST_DIR" -mindepth 2 -type f -exec mv -n {} "$DIST_DIR/" \;
find "$DIST_DIR" -mindepth 1 -type d -empty -delete
# Flatten any residual platform subdirs inside extras/ and drop empties.
find "$DIST_DIR/extras" -mindepth 2 -type f -exec mv -n {} "$DIST_DIR/extras/" \;
find "$DIST_DIR/extras" -mindepth 1 -type d -empty -delete
rmdir "$DIST_DIR/extras" 2>/dev/null || true

echo ""
echo "Generating SHA-256 checksums..."
Expand Down Expand Up @@ -143,11 +155,12 @@ gpg --verify <file>.asc <file>
fi

echo "Creating GitHub release $TAG..."
mapfile -t release_assets < <(find "$DIST_DIR" -maxdepth 1 -type f)
gh release create "$TAG" \
--repo "$REPO" \
--title "Alipsa Accounting ${VERSION}" \
--notes "$release_body" \
"$DIST_DIR"/*
"${release_assets[@]}"

echo ""
release_url=$(gh release view "$TAG" --repo "$REPO" --json url --jq '.url')
Expand Down
Loading