Merge branch 'master' into release

This commit is contained in:
Jarek Sacha 2023-06-11 21:32:48 -04:00
commit 4e3fbe32a8
No known key found for this signature in database
GPG key ID: F29625CE62288163
20 changed files with 445 additions and 74 deletions

View file

@ -33,7 +33,10 @@ the logic flow is too complex to correct without a significant rewrite.
Features
--------
* Uses similar options to the original ImageJ Launcher, si IJP Launcher can be drop-in replacement
Here are the futures that are already implemented (see release notes for futures ofa specific release):
* Uses similar options to the original ImageJ Launcher, so IJP Launcher can be used as a drop-in replacement
* Intended to be used with Java 11 or newer (the original launcher can be used for Java 8)
* Provides native executable for various OS/Hardware systems
- Windows
- Mac OS X Arm64 (Apple Silicon)
@ -48,7 +51,7 @@ Features
- Search ImageJ directory for available Java executables
* Determines the amount of memory used by JVM based on total system memory use 75% of the max
* Determines available `imagej-launcher*.jar`
* Performs updates pending after the last time ImageJ was closed
* **Performs updates** pending after the last time ImageJ was closed
Full List of Command Line Options
---------------------------------
@ -116,9 +119,22 @@ and "IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command", save them to the `Fiji.app
The "*.command" file is a helper that can be used to launch Fiji without using command prompt.
Future versions of the IJP Launcher, after v.0.1.0, may eliminate the need for using this file.
**7. Start ImageJ**
**7. Set Executable Permissions**
In the `Fiji.app` folder double-click on `IJP-ImageL-Launcher-0.1.0-macosx-arm64.command` file (note the extension "*
When you download launcher files they may be saved without executable permissions.
* Open terminal
* Navigate to the Fiji.app folder, for instance, `cd ~/Download/Fiji.app`
* Add executable permission to the launcher and the "*.command" file using
```shell
chmod +x IJP-ImageJ-Launcher-0.1.0-macosx-arm64
chmod +x IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command
```
**8. Start ImageJ**
You can start `IJP-ImageL-Launcher-0.1.0-macosx-arm64` from command line or in the `Fiji.app` folder double-click on `IJP-ImageL-Launcher-0.1.0-macosx-arm64.command` file (note the extension "*
.command")
That should start Fiji.
You may need to open Settings and allow the IJP ImageJ Launcher to run.

View file

@ -1,6 +1,6 @@
scalaVersion := "3.3.0"
scalaVersion := "3.3.0"
//name := "IJP-ImageJ-Launcher"
version := "0.1.0"
version := "0.1.1-SNAPSHOT"
versionScheme := Some("early-semver")
organization := "net.sf.ij-plugins"
homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL)
@ -16,14 +16,26 @@ enablePlugins(ScalaNativePlugin)
logLevel := Level.Info
libraryDependencies ++= Seq(
"com.github.scopt" %%% "scopt" % "4.1.0",
"com.lihaoyi" %%% "os-lib" % "0.9.1"
"com.github.scopt" %%% "scopt" % "4.1.0",
"com.lihaoyi" %%% "os-lib" % "0.9.1",
"org.scalatest" %%% "scalatest" % "3.2.16" % Test
)
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-explain",
"-explain-types",
"-rewrite",
"-source:3.3-migration",
// "-Wvalue-discard",
"-Wunused:all"
)
Compile / run / mainClass := Some("ij_plugins.imagej_launcher.Main")
// import to add Scala Native options
import scala.scalanative.build._
import scala.scalanative.build.*
// defaults set with common options shown
nativeConfig ~= { c =>
@ -36,3 +48,8 @@ nativeConfig ~= { c =>
//nativeConfig ~= { c =>
// c.withCompileOptions(c.compileOptions ++ Seq("-v"))
//}
// Version info generation from SBT configuration
enablePlugins(BuildInfoPlugin)
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion)
buildInfoPackage := "ij_plugins.imagej_launcher"

View file

@ -1,4 +1,4 @@
#!/bin/bash
DIR=$(cd "$(dirname "$0")" && pwd -P)
echo "$DIR"
"$DIR"/IJP-ImageL-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"
"$DIR"/IJP-ImageJ-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"

5
notes/v.0.2.0.md Normal file
View file

@ -0,0 +1,5 @@
### New Features
* Use launcher location to infer `ij-dir`. #5
* Session log is saved to `~/.ijp_imagej_launcher.log` to facilitate troubleshooting when not running from command
prompt. The log is reset for each session. #6

View file

@ -1 +1,6 @@
sbt.version = 1.8.3
#
# Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
# Author's e-mail: jpsacha at gmail.com
#
sbt.version = 1.9.0

2
project/buildinfo.sbt Normal file
View file

@ -0,0 +1,2 @@
// https://github.com/sbt/sbt-buildinfo
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")

View file

@ -1 +1,2 @@
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12")
// https://github.com/scala-native/scala-native
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14")

View file

@ -0,0 +1,40 @@
#include <stdio.h>
#if defined(__linux__)
#include <unistd.h>
#include <limits.h>
#elif defined(__APPLE__)
#include <stdint.h>
#include <sys/syslimits.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
size_t path_max() {
#ifdef _WIN32
return MAX_PATH;
#else
return PATH_MAX + 1;
#endif
}
void get_exe_path(char* exe_path, size_t size) {
exe_path[0] = 0;
#if defined(__linux__)
// char arg1[20];
// sprintf( arg1, "/proc/%d/exe", getpid() );
// printf("arg1 = %d\n", arg1);
// readlink( arg1, exepath, PATH_MAX );
readlink( "/proc/self/exe", exe_path, size );
#elif defined(__APPLE__)
if (_NSGetExecutablePath(exe_path, &size) != 0)
printf("ERROR: buffer too small; need size %lu\n", size);
#elif defined(_WIN32)
unsigned int len = GetModuleFileNameA(GetModuleHandleA(0x0), exe_path, size);
if (len == 0) // memory not sufficient or general error occurred
printf("ERROR: buffer too small or general error.\n");
#endif
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
* Author's e-mail: jpsacha at gmail.com
*/
package ij_plugins.imagej_launcher
import os.Path
object IJConfigFile:
private val FileName: String = "ImageJ.cfg"
private val MagicHeader: String = "# ImageJ startup properties"
private val MaxHeapMBKey: String = "maxheap.mb"
private val JvmArgsKey: String = "jvmargs"
private val LegacyModeKey: String = "legacy.mode"
private val AssignmentSymbol: String = "="
private val CommentSymbol: String = "#"
/**
* Read `ImageJ.cfg` from a specified directory.
* The configuration file must be in ImageJ2 format (with magic header "# ImageJ startup properties").
*
* @param dir directory where `ImageJ.cfg` is located
* @param logger logger
* @return option containing the configuration file (Right).
* The option is empty if file is not present in input `dir`.
* An error message is returned if file is present but cannot be read without errors (Left).
*/
def readFromDir(dir: Path)(using logger: Logger): Either[String, Option[IJCConfig]] =
logger.debug(s"Reading ImageJ configuration from directory: $dir")
val path = dir / FileName
logger.debug(s" Looking for $FileName in: '$path'")
readFromFile(path)
private[imagej_launcher] def readFromFile(ijConfigFile: Path)(using logger: Logger): Either[String, Option[IJCConfig]] =
if os.exists(ijConfigFile) then
val lines = os.read.lines(ijConfigFile)
val cfgE =
lines.headOption match
case Some(line) =>
if line.trim.startsWith(MagicHeader) then
logger.debug(s" Parsing $FileName")
for
props <- parseProps(lines)
maxHeapMB <- parseLong(props, MaxHeapMBKey)
jvmArgs <- parseString(props, JvmArgsKey)
legacyMode <- parseBoolean(props, LegacyModeKey)
yield Option(IJCConfig(maxHeapMB, jvmArgs, legacyMode))
else
Left(s"$FileName is invalid, does not contain header: '$MagicHeader'")
case None =>
Left(s"$FileName is empty.")
// Add context to the error message, if there is one
cfgE.left.map(e => s"Failed to read $FileName. $e")
else
logger.debug(s" $FileName not found: $ijConfigFile")
Right(None)
private[imagej_launcher] def parseProps(lines: Seq[String]): Either[String, Map[String, String]] =
// Parse each line
val lineResults: Seq[Either[String, (String, String)]] =
lines
.zipWithIndex
.filter { case (line, _) => !line.startsWith(CommentSymbol) }
.map { case (line, lineNumber) =>
val index = line.indexOf(AssignmentSymbol)
if index > 0 then
val key = line.substring(0, index).trim
if !key.isBlank then
Right(key -> line.substring(index + 1).trim)
else
Left(s"Error in line $lineNumber: key cannot be empty: '$line'")
else if index == 0 then
Left(s"Error in line $lineNumber: line cannot start with an assignment symbol '$AssignmentSymbol': '$line'")
else
Left(s"Error in line $lineNumber: no assignment symbol '$AssignmentSymbol' present: '$line'")
}
// Separate lines with errors
val errors = lineResults.collect { case e: Left[?, ?] => e }
if errors.isEmpty then
// Convert key->value pairs to a map
val r =
lineResults
.collect { case e: Right[?, ?] => e }
.map(_.value)
.toMap
Right(r)
else
Left(errors.map(_.value).mkString("\n"))
private def parseLong(props: Map[String, String], key: String): Either[String, Long] =
parseString(props, key).flatMap: s =>
s.toLongOption match
case Some(v) => Right(v)
case None => Left(s"Failed to parse '$s' as an integer.")
private def parseString(props: Map[String, String], key: String): Either[String, String] =
props.get(key) match
case Some(v) => Right(v)
case None => Left(s"No key named '$key'.")
private def parseBoolean(props: Map[String, String], key: String): Either[String, Boolean] =
parseString(props, key).flatMap: s =>
s.toBooleanOption match
case Some(v) => Right(v)
case None => Left(s"Failed to parse '$s' as a boolean.")
/** Configuration loaded from `ImageJ.cfg`. */
case class IJCConfig(maxHeapMB: Long, jvmArgs: String, legacyMode: Boolean)
end IJConfigFile

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
* Author's e-mail: jpsacha at gmail.com
*/
package ij_plugins.imagej_launcher
import ij_plugins.imagej_launcher.Main.Config
import os.{FilePath, Path}
import scala.util.control.NonFatal
object IJDir:
/** Name of an ImageJ sub-directory containing jars */
val jarsDirName = "jars"
/** Locate ImageJ directory */
def locate(config: Config)(using logger: Logger): Either[String, Path] =
logger.debug("Looking for ImageJ directory")
config.ijDir match
case Some(d) =>
logger.debug(s" Considering provided ij-dir: '$d'")
asPath(d.getPath).flatMap: p =>
logger.debug(s" '$p'")
if isIJDir(p) then Right(p)
else Left(s"ij-dir is not an ImageJ directory [$p]")
case None =>
logger.debug(" Considering application directory")
val appPath = Native.applicationPath()
logger.debug(s" Application directory: '$appPath'")
inferIJDir(appPath) match
case Some(p) =>
Right(p)
case None =>
logger.debug(" Application directory is not an ImageJ directory")
logger.debug(" Considering current working directory")
val cwd = os.pwd
logger.debug(s" Current working directory: '$cwd'")
inferIJDir(cwd) match
case Some(p) =>
Right(p)
case None =>
logger.debug(" Current working directory is not an ImageJ directory.")
Left("Cannot locate ImageJ directory.")
private def inferIJDir(path: Path): Option[Path] =
if isIJDir(path) then
Option(path)
else if path.endsWith(os.rel / "Contents" / "MacOS") then
Option(path / os.up / os.up)
else
None
private def isIJDir(path: Path): Boolean =
os.exists(path) &&
os.isDir(path) &&
os.list(path).exists(f => f.last == jarsDirName && os.isDir(f))
private def asPath(filePath: String): Either[String, Path] =
if filePath.trim.startsWith("~") then
try
Right(Path.expandUser(filePath))
catch
case NonFatal(ex) =>
Left(s"Not an absolute path: '$filePath' [${ex.getMessage}]")
else
FilePath(filePath) match
case p: Path => Right(p)
case _ => Left(s"Not an absolute path: '$filePath'")
end IJDir

View file

@ -5,6 +5,7 @@
package ij_plugins.imagej_launcher
import ij_plugins.imagej_launcher.IJDir.jarsDirName
import ij_plugins.imagej_launcher.Launcher.javaExeFileName
import ij_plugins.imagej_launcher.Main.Config
import os.Path
@ -12,9 +13,7 @@ import os.Path
import java.io.File
import java.lang.ProcessBuilder.Redirect
class Launcher(logger: Logger):
private val jarsDirName = "jars"
class Launcher(using logger: Logger):
def run(config: Config): Unit =
prepareLaunch(config) match
@ -29,33 +28,16 @@ class Launcher(logger: Logger):
private def prepareLaunch(config: Config): Either[String, Seq[String]] =
for
ijDir <- locateIJDir(config)
_ <- Updater.update(Path(ijDir), config.dryRun, logger)
launcherJar <- findImageJLauncherJar(ijDir)
javaExe <- locateJavaExecutable(config, ijDir)
ijDir <- IJDir.locate(config)
_ <- Updater.update(ijDir, config.dryRun)
ijConfig <- IJConfigFile.readFromDir(ijDir)
launcherJar <- findImageJLauncherJar(ijDir.toIO)
javaExe <- locateJavaExecutable(config, ijDir.toIO)
systemType <- determineSystemType()
yield
val maxMemoryMB = determineMaxMemoryMB()
logger.info(s"Max memory to use: ${maxMemoryMB}MB")
buildCommandLine(ijDir, javaExe, launcherJar, systemType, maxMemoryMB)
private def locateIJDir(config: Config): Either[String, File] =
logger.debug("Looking for ImageJ directory")
val dir = config.ijDir match
case Some(d) =>
logger.debug(" Considering provided ij-dir")
d
case None =>
logger.debug(" Considering current directory")
new File(".").getCanonicalFile
dir.listFiles().find(f => f.getName == jarsDirName && f.isDirectory) match
case Some(_) =>
logger.info(s" ImageJ directory set to: '$dir'")
Right(dir)
case None =>
Left(s"Cannot locate ImageJ directory. No subdirectory '$jarsDirName' in '$dir''")
buildCommandLine(ijDir.toIO, javaExe, launcherJar, systemType, maxMemoryMB)
private def findImageJLauncherJar(ijDir: File): Either[String, File] =
logger.debug("Looking for 'imagej-launcher*.jar'")
@ -119,7 +101,7 @@ class Launcher(logger: Logger):
p.toIO.getName == javaExeFileName &&
p.toIO.getParentFile.getName == "bin"
)
logger.debug(" Candidates: " + c2.mkString(", "))
logger.debug(" Candidates: " + c2.mkString(", "))
c2.map(_.toIO.getParentFile.getParentFile)
.headOption
else
@ -197,6 +179,7 @@ class Launcher(logger: Logger):
"plugins",
"net.imagej.Main"
)
end buildCommandLine
private def launch(command: Seq[String]): Unit =
logger.debug("launchImageJ ...")

View file

@ -5,20 +5,60 @@
package ij_plugins.imagej_launcher
import ij_plugins.imagej_launcher.Logger.Level
import ij_plugins.imagej_launcher.Logger.{Level, logToFile}
import os.Path
import java.nio.file.{Files, StandardOpenOption}
import scala.util.control.NonFatal
class Logger(val level: Level = Level.Info):
def debug(msg: String): Unit = pprint(Level.Debug, msg)
def info(msg: String): Unit = pprint(Level.Info, msg)
def error(msg: String): Unit = pprint(Level.Error, msg)
private def pprint(l: Level, message: String): Unit =
val m = f"${l.name}%-5s: $message"
if l.level <= level.level then println(f"${l.name}%-5s: $message")
logToFile(m)
object Logger:
private val LogFile: Path = os.home / ".ijp_imagej_launcher.log"
private var logFileReset: Boolean = true
enum Level(val level: Int, val name: String):
case Off extends Level(0, "OFF")
case Error extends Level(200, "ERROR")
case Info extends Level(400, "INFO")
case Debug extends Level(500, "DEBUG")
case All extends Level(Int.MaxValue, "DEBUG")
private def logToFile(message: String): Unit =
try
if logFileReset && os.exists(LogFile) then
val _ = os.remove(LogFile)
logFileReset = false
val data = readableTimeStamp() + ": " + message + "\n"
val lines = data.getBytes()
val _ = Files.write(
LogFile.toNIO,
lines,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
StandardOpenOption.WRITE
)
catch
case NonFatal(_) =>
// Ignore exceptions
private def readableTimeStamp(): String =
// Simple implementation returns GMT time (no date)
// Use custom code as DateTimeFormatter is not available in Scala Native 0.4.14
val t = System.currentTimeMillis()
val sss = t % 1000
val ss = (t / 1000L).toInt % 60
val mm = (t / (1000L * 60L)).toInt % 60
val hh = (t / (1000L * 60L * 60L)).toInt % 24
f"$hh%02d-$mm%02d-$ss%02d_$sss%03dZ"

View file

@ -5,15 +5,14 @@
package ij_plugins.imagej_launcher
import scopt.{DefaultOEffectSetup, OParser}
import scopt.OParser
import java.io.File
object Main:
private var logger = new Logger()
private val AppName = "ijp-imagej-launcher"
// private val AppVersion = s"${Version.version} [${Version.buildTimeStr}]"
private val AppVersion = s"0.1.0"
private var logger = new Logger()
private val AppName = "IJP-ImageJ-Launcher"
private val AppVersion = BuildInfo.version
private val VersionMessage = s"v.$AppVersion"
private val AppDescription =
"""Native launcher for ImageJ2
@ -29,6 +28,7 @@ object Main:
case None =>
private def parseCommandLine(args: Array[String]): Option[Config] =
logger.debug("Command line: " + args.map("'" + _ + "'").mkString(", "))
val builder = OParser.builder[Config]
val parser1 =
import builder.*
@ -69,7 +69,7 @@ object Main:
private def setupLogger(logLevel: Logger.Level): Unit = logger = new Logger(logLevel)
private def runLauncher(config: Config): Unit = new Launcher(logger).run(config)
private def runLauncher(config: Config): Unit = new Launcher(using logger).run(config)
case class Config(
logLevel: Logger.Level = Logger.Level.Error,

View file

@ -1,9 +1,29 @@
package ij_plugins.imagej_launcher
import scala.scalanative.unsafe.extern
import os.{Path, up}
import scala.scalanative.unsafe
import scala.scalanative.unsafe.*
object Native:
def applicationPath(): Path =
val maxPath: CSize = argv0.path_max()
// use Zone to manage native memory
val exePath =
Zone { implicit z =>
val buffer: CString = alloc[CChar](maxPath)
argv0.get_exe_path(buffer, maxPath)
unsafe.fromCString(buffer)
}
Path(exePath) / up
@extern
object mem:
def determineTotalSystemMemory(): Long = extern
@extern
private object argv0:
def path_max(): CSize = extern
def get_exe_path(exe_path: CString, size: CSize): Unit = extern

View file

@ -1,6 +1,11 @@
/*
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
* Author's e-mail: jpsacha at gmail.com
*/
package ij_plugins.imagej_launcher
import os.{Path, RelPath}
import os.Path
import scala.util.control.NonFatal
@ -13,7 +18,7 @@ object Updater:
* @param logger configured logger
* @return Number of files processed or an error message.
*/
def update(ijDir: Path, dryRun: Boolean, logger: Logger): Either[String, Long] =
def update(ijDir: Path, dryRun: Boolean)(using logger: Logger): Either[String, Long] =
try
val updateDir = ijDir / "update"
// Count used only for debug info
@ -23,8 +28,7 @@ object Updater:
os.walk(updateDir)
.filter(os.isFile)
.foreach: src =>
// val relativeDir = src.relativeTo(updateDir)
val dst = ijDir / relativeTo(src, updateDir)
val dst = ijDir / src.relativeTo(updateDir)
if os.size(src) == 0 then
logger.debug(s"remove: $dst")
if !dryRun then os.remove(dst)
@ -35,7 +39,7 @@ object Updater:
if !dryRun then os.move(src, dst, replaceExisting = true, createFolders = true)
count += 1
logger.debug(s"Delete update directory: $updateDir")
if !dryRun then deleteEmptyDirs(updateDir, logger)
if !dryRun then deleteEmptyDirs(updateDir)
Right(count)
else
logger.info("No update found")
@ -45,30 +49,11 @@ object Updater:
ex.printStackTrace()
Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}")
private def relativeTo(src: Path, base: Path): RelPath =
// This does what src.relativeTo(base) should do
// Problems is in the native code on Windows,
// os.Path#relativeTo creates relative path by adding `../` at the beginning of the absolute path,
// co you may get `../C:\a\b` which leads to a exception soon after.
// The issue is with Scala Native implementation of java.nio.file.Path#relativize on Windows,
// See https://github.com/scala-native/scala-native/issues/3293
// This implementation is very limited but sufficient for our use.
// It assumes specific relation between src and base.
val srcStr = src.toString
val baseStr = base.toString
require(baseStr.nonEmpty)
require(srcStr.startsWith(baseStr))
val relStr = srcStr.drop(baseStr.length + 1)
RelPath(relStr)
private def deleteEmptyDirs(dir: Path, logger: Logger): Unit =
private def deleteEmptyDirs(dir: Path)(using logger: Logger): Unit =
logger.debug(s"Cleaning directory: $dir")
os.list(dir)
.filter(os.isDir)
.foreach(p => deleteEmptyDirs(p, logger))
.foreach(p => deleteEmptyDirs(p))
if os.list(dir).isEmpty then
logger.debug(s"Removing empty dir: $dir")

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
* Author's e-mail: jpsacha at gmail.com
*/
package ij_plugins.imagej_launcher
import org.scalatest.flatspec.AnyFlatSpec
class IJConfigFileTest extends AnyFlatSpec:
given logger:Logger = new Logger(Logger.Level.All)
"IJConfigFile" should "read valid config files" in:
val path = os.pwd / "test" / "data" / "config_valid" / "ImageJ.cfg"
IJConfigFile.readFromFile(path) match
case Right(cfg) =>
logger.debug(cfg.toString)
assert(cfg.nonEmpty)
case Left(err) =>
logger.debug(s"Unexpected error: $err")
fail()
it should "fail reading invalid config files" in:
val path = os.pwd / "test" / "data" / "config_invalid" / "ImageJ.cfg"
IJConfigFile.readFromFile(path) match
case Right(cfg) =>
logger.debug(cfg.toString)
fail(s"Expected to return errors when reading config, but read config as: $cfg")
case Left(err) =>
logger.debug(s"Expected error\n:$err")

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
* Author's e-mail: jpsacha at gmail.com
*/
package ij_plugins.imagej_launcher
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should
import java.nio.file.FileSystems
import scala.scalanative.runtime.Platform.isWindows
class PathSpec extends AnyFlatSpec with should.Matchers:
"A Path" should "should `relativize` jars on Windows (Scala Native #3293)" in {
if isWindows() then
// This to test that fix for Scala Native #3293 implemented, it should be part of Scala Native 0.4.13
// See https://github.com/scala-native/scala-native/issues/3293
// val src = Path.of("C:\\a\\b\\c.jar")
val src = FileSystems.getDefault.getPath("C:\\a\\b\\c.jar")
// val base = Path.of("C:\\a")
val base = FileSystems.getDefault.getPath("C:\\a")
val rel = base.relativize(src)
rel.toString should be("b\\c.jar")
}

View file

@ -4,6 +4,7 @@ import os.Path
@main
def updaterDemo(ijDir: String): Unit =
Updater.update(Path(ijDir), dryRun = false, new Logger(Logger.Level.Debug)) match
given logger: Logger = new Logger(Logger.Level.Debug)
Updater.update(Path(ijDir), dryRun = false) match
case Right(count) => println(s"Processed $count files")
case Left(error) => println(s"Failed with error: $error")

View file

@ -0,0 +1,4 @@
# ImageJ startup properties
maxheap.mb = 1024m
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
legacy.mode = false

View file

@ -0,0 +1,4 @@
# ImageJ startup properties
maxheap.mb = 1024
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
legacy.mode = false