diff --git a/.gitignore b/.gitignore index ff4fe56..97be7d7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ .idea/ target/ -.DS_Store diff --git a/.scalafmt.conf b/.scalafmt.conf index 12f6d23..7ee8611 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.8.3 +version = 3.7.4 runner.dialect = scala3 diff --git a/ReadMe.md b/ReadMe.md index a146012..ac323cc 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -15,8 +15,6 @@ IJP ImageJ Launcher is a clean implementation on the core function of starting [ * [Installing Fiji on Mac OS X Arm64](#installing-fiji-on-mac-os-x-arm64) * [Installing Fiji on Windows x64](#installing-fiji-on-windows-x64) * [Troubleshooting](#troubleshooting) - * [Start-up log `~/.ijp_imagej_launcher.log`](#start-up-log-ijpimagejlauncherlog) - * [Starting from command prompt](#starting-from-command-prompt) * [Developer Setup](#developer-setup) @@ -35,10 +33,7 @@ the logic flow is too complex to correct without a significant rewrite. Features -------- -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) +* Uses similar options to the original ImageJ Launcher, si IJP Launcher can be drop-in replacement * Provides native executable for various OS/Hardware systems - Windows - Mac OS X Arm64 (Apple Silicon) @@ -53,7 +48,7 @@ Here are the futures that are already implemented (see release notes for futures - 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 --------------------------------- @@ -82,7 +77,7 @@ This example will show how to: **1. Download FIJI without JRE** -Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS). +Go to https://fiji.sc/ and select "Download the no-JRE version". That should get file called `fiji-nojre.zip` **2. Unzip the `fiji-nojre.zip` in a folder of choice** @@ -100,49 +95,41 @@ Inside the `Fiji.app` folder create a new folder called `java`. In browser open https://adoptium.net/temurin/releases/ Select: -* Operating System: `macOS` -* Architecture: `aarch64` also known as Apple Silicon or Arm64 -* Package Type: `JRE` (`JDK` is fine too, is larger and supports Java compilation) -* Version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it) +* operating system: `macOS` +* architecture: `aarch64` also known as Apple Silicon or Arm64 +* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation) +* version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it) Click on `tar.gz` button to download and save to the `java` folder you created earlier. -You should have file like `OpenJDK11U-jre_aarch64_mac_hotspot_11.0.20_8.tar.gz`. +You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.tar.gz`. -**5. Uncompress into the `Fiji.app/java` folder** +**5. Uncompress into the `java` folder** -That will create folder like `jdk-11.0.20+8-jre`. +That will create folder like `jdk-11.0.19+7-jre`. This is the Java VM that IJP ImageJ Launcher will use to start Fiji. -**6. Download the IJP ImageJ Launcher and uncompress** +**6. Download the IJP ImageJ Launcher to the Fiji.app directory** -Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip" +Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-macosx-arm64" +and "IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command", save them to the `Fiji.app` folder. -Uncompress "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip". -Inside you will get `ImageJ-macosx`. +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. Add to Fiji.app** +**7. Start ImageJ** -Inside `Fiji.app` locate folder `Contents/MacOS`. +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. -Copy `ImageJ-macosx` to the `Contents/MacOS` folder, replacing `ImageJ-macosx` that was there. - -**8. Move Fiji.app to the Application folder** - -At this point you can move the `Fiji.app` folder to the Applications folder and use is as a regular msOS application. - -**9. Troubleshooting** - -When you attempt to run Fiji with the new Launcher you may get a warning dialog -![macOS_warning_dialog_01.png](assets%2FmacOS_warning_dialog_01.png) - -Possible work-around - -1. Delete `Fuji.app` folder -2. Uncompressed `fiji-nojre.zip` to recreate `Fuji.app` folder, but do not make any changes to it yet. You may need to do it is different folder than before. -3. Control-clock on `Fuji.app` and select "Open". You will see dialog saying - "macOS cannot verify the developer of “Fiji”. Are you sure you want to open it?" -4. Click on "Open". You will see Fiji logo, but the application will close since it is not setup yet -5. Now you can repeat steps "3. Create place for Java (JRE)" to "7. Add to Fiji.app" above +You can also create an alis on the Desktop to avoid navigating to the `Fiji.app` folder each time. +Using Finder, press `Option`+`Command` and drag the *.command file to the Desktop. +The original *.command file will stay were it is and a new icon/alias (wth a little arrow at the bottom) will be created +on the Desktop. +Now you can double-click on the new alias on the Desktop to start Fiji. +You can rename the Desktop alias to whatever you like, for instance, `Fiji`, but do not change names of the downloaded +files, otherwise the alias (and *.command) may no longer work, and you will need to use terminal to start the launcher. If you have problems installing, please report in [Discussions] or [Image.sc Forum] @@ -156,7 +143,7 @@ This example will show how to: **1. Download FIJI without JRE** -Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS). +Go to https://fiji.sc/ and select "Download the no-JRE version". That should get file called `fiji-nojre.zip` **2. Unzip the `fiji-nojre.zip` in a folder of choice** @@ -173,31 +160,31 @@ Inside the `Fiji.app` folder create a new folder called `java`. In browser open https://adoptium.net/temurin/releases/ Select: -* Operating System: `Windows` -* Architecture: `x64` also known as Apple Silicon or Arm64 -* Package Type: `JRE` (`JDK` is fine too, is larger and supports Java compilation) -* Version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it) +* operating system: `Windows` +* architecture: `x64` also known as Apple Silicon or Arm64 +* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation) +* version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it) Click on `.zip` button to download and save to the `java` folder you created earlier. -You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.20_8.zip`. +You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.zip`. -**5. Uncompress into the `Fiji.app/java` folder** +**5. Uncompress into the `java` folder** -That will create folder like `jdk-11.0.20+8-jre`. +That will create folder like `jdk-11.0.19+7-jre`. This is the Java VM that IJP ImageJ Launcher will use to start Fiji. **6. Download the IJP ImageJ Launcher to the Fiji.app directory** -Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-windows_x64.exe", save it to the `Fiji.app` folder. +Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-windows_x64.exe", save it to the `Fiji.app` folder. **7. Start ImageJ** -In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.2.0-windows_x64.exe`. +In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.1.0-windows_x64.exe`. That should start Fiji. You can also create a shortcut on the Desktop to avoid navigating to the `Fiji.app` folder each time. -**_Left_**-click on the `IJP-ImageJ-Launcher-0.2.0-windows_x64.exe` and drag it to the Desktop. +**_Left_**-click on the `IJP-ImageJ-Launcher-0.1.0-windows_x64.exe` and drag it to the Desktop. Once you release mouse button, a pop-up manu will open, select "Create shortcut here". Now you can double-click on the new shortcut on the Desktop to start Fiji. @@ -207,13 +194,6 @@ If you have problems installing, please report in [Discussions] or [Image.sc For ### Troubleshooting -#### Start-up log `~/.ijp_imagej_launcher.log` - -The IJP-ImageJ-Launcher writes diagnostic info to a file `.ijp_imagej_launcher.log` in the users home directory. -The information recorded is some as using `--debug` on command line. - -#### Starting from command prompt - You can start the IJP Image Launcher from the terminal and see diagnostic printouts that may help troubleshoot potential issues. diff --git a/assets/macOS_warning_dialog_01.png b/assets/macOS_warning_dialog_01.png deleted file mode 100644 index a5f3051..0000000 Binary files a/assets/macOS_warning_dialog_01.png and /dev/null differ diff --git a/build.sbt b/build.sbt index e6be4c6..a81f4c8 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ -scalaVersion := "3.3.3" +scalaVersion := "3.3.0" //name := "IJP-ImageJ-Launcher" -version := "0.2.0.1-SNAPSHOT" +version := "0.1.0" versionScheme := Some("early-semver") organization := "net.sf.ij-plugins" homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL) @@ -16,26 +16,14 @@ enablePlugins(ScalaNativePlugin) logLevel := Level.Info libraryDependencies ++= Seq( - "com.github.scopt" %%% "scopt" % "4.1.0", - "com.lihaoyi" %%% "os-lib" % "0.9.3", - "org.scalatest" %%% "scalatest" % "3.2.18" % Test -) - -scalacOptions ++= Seq( - "-unchecked", - "-deprecation", - "-explain", - "-explain-types", - "-rewrite", - "-source:3.3-migration", -// "-Wvalue-discard", - "-Wunused:all" + "com.github.scopt" %%% "scopt" % "4.1.0", + "com.lihaoyi" %%% "os-lib" % "0.9.1" ) 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 => @@ -48,8 +36,3 @@ 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" diff --git a/extras/IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command b/extras/IJP-ImageL-Launcher-0.1.0-macosx-arm64.command similarity index 51% rename from extras/IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command rename to extras/IJP-ImageL-Launcher-0.1.0-macosx-arm64.command index ebcfb9e..4432ceb 100755 --- a/extras/IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command +++ b/extras/IJP-ImageL-Launcher-0.1.0-macosx-arm64.command @@ -1,4 +1,4 @@ #!/bin/bash DIR=$(cd "$(dirname "$0")" && pwd -P) echo "$DIR" -"$DIR"/IJP-ImageJ-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR" +"$DIR"/IJP-ImageL-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR" diff --git a/notes/v.0.2.0.md b/notes/v.0.2.0.md deleted file mode 100644 index 2dbf490..0000000 --- a/notes/v.0.2.0.md +++ /dev/null @@ -1,8 +0,0 @@ -Feature release: support better system integration on macOS - support installing ImageJ/Fiji in the application -directory. -See macOS installation info in the [ReadMe](https://github.com/ij-plugins/ijp-imagej-launcher) - -* 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 -* Better inference of ImageJ directory on macOS - consider launcher being in subdirectory "Contents/MacOS" #7 diff --git a/project/build.properties b/project/build.properties index 7a2a0c9..ef3d266 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,6 +1 @@ -# -# Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved. -# Author's e-mail: jpsacha at gmail.com -# - -sbt.version = 1.10.2 +sbt.version = 1.8.3 diff --git a/project/buildinfo.sbt b/project/buildinfo.sbt deleted file mode 100644 index dab5b61..0000000 --- a/project/buildinfo.sbt +++ /dev/null @@ -1,2 +0,0 @@ -// https://github.com/sbt/sbt-buildinfo -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 6264039..f07f01d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1 @@ -// https://github.com/scala-native/scala-native -resolvers ++= Resolver.sonatypeOssRepos("snapshots") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") diff --git a/src/main/resources/scala-native/argv0.c b/src/main/resources/scala-native/argv0.c deleted file mode 100644 index f073dd8..0000000 --- a/src/main/resources/scala-native/argv0.c +++ /dev/null @@ -1,40 +0,0 @@ - -#include - -#if defined(__linux__) -#include -#include -#elif defined(__APPLE__) -#include -#include -#elif defined(_WIN32) -#include -#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 -} \ No newline at end of file diff --git a/src/main/scala/ij_plugins/imagej_launcher/IJConfigFile.scala b/src/main/scala/ij_plugins/imagej_launcher/IJConfigFile.scala deleted file mode 100644 index 203d9ca..0000000 --- a/src/main/scala/ij_plugins/imagej_launcher/IJConfigFile.scala +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 diff --git a/src/main/scala/ij_plugins/imagej_launcher/IJDir.scala b/src/main/scala/ij_plugins/imagej_launcher/IJDir.scala deleted file mode 100644 index e871ea1..0000000 --- a/src/main/scala/ij_plugins/imagej_launcher/IJDir.scala +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 diff --git a/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala b/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala index 694d183..2c8d654 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala @@ -5,7 +5,6 @@ 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 @@ -13,7 +12,9 @@ import os.Path import java.io.File import java.lang.ProcessBuilder.Redirect -class Launcher(using logger: Logger): +class Launcher(logger: Logger): + + private val jarsDirName = "jars" def run(config: Config): Unit = prepareLaunch(config) match @@ -28,16 +29,33 @@ class Launcher(using logger: Logger): private def prepareLaunch(config: Config): Either[String, Seq[String]] = for - ijDir <- IJDir.locate(config) - _ <- Updater.update(ijDir, config.dryRun) - ijConfig <- IJConfigFile.readFromDir(ijDir) - launcherJar <- findImageJLauncherJar(ijDir.toIO) - javaExe <- locateJavaExecutable(config, ijDir.toIO) + ijDir <- locateIJDir(config) + _ <- Updater.update(Path(ijDir), config.dryRun, logger) + launcherJar <- findImageJLauncherJar(ijDir) + javaExe <- locateJavaExecutable(config, ijDir) systemType <- determineSystemType() yield val maxMemoryMB = determineMaxMemoryMB() logger.info(s"Max memory to use: ${maxMemoryMB}MB") - buildCommandLine(ijDir.toIO, javaExe, launcherJar, systemType, maxMemoryMB) + 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''") private def findImageJLauncherJar(ijDir: File): Either[String, File] = logger.debug("Looking for 'imagej-launcher*.jar'") @@ -101,7 +119,7 @@ class Launcher(using 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 @@ -179,7 +197,6 @@ class Launcher(using logger: Logger): "plugins", "net.imagej.Main" ) - end buildCommandLine private def launch(command: Seq[String]): Unit = logger.debug("launchImageJ ...") diff --git a/src/main/scala/ij_plugins/imagej_launcher/Logger.scala b/src/main/scala/ij_plugins/imagej_launcher/Logger.scala index 9963f40..baa5390 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Logger.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Logger.scala @@ -5,60 +5,20 @@ package ij_plugins.imagej_launcher -import ij_plugins.imagej_launcher.Logger.{Level, logToFile} -import os.Path - -import java.nio.file.{Files, StandardOpenOption} -import scala.util.control.NonFatal +import ij_plugins.imagej_launcher.Logger.Level 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" diff --git a/src/main/scala/ij_plugins/imagej_launcher/Main.scala b/src/main/scala/ij_plugins/imagej_launcher/Main.scala index d2afaa8..355f25c 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Main.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Main.scala @@ -5,14 +5,15 @@ package ij_plugins.imagej_launcher -import scopt.OParser +import scopt.{DefaultOEffectSetup, OParser} import java.io.File object Main: - private var logger = new Logger() - private val AppName = "IJP-ImageJ-Launcher" - private val AppVersion = BuildInfo.version + 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 val VersionMessage = s"v.$AppVersion" private val AppDescription = """Native launcher for ImageJ2 @@ -28,7 +29,6 @@ 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(using logger).run(config) + private def runLauncher(config: Config): Unit = new Launcher(logger).run(config) case class Config( logLevel: Logger.Level = Logger.Level.Error, diff --git a/src/main/scala/ij_plugins/imagej_launcher/Native.scala b/src/main/scala/ij_plugins/imagej_launcher/Native.scala index 3c82706..2e847c4 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Native.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Native.scala @@ -1,29 +1,9 @@ package ij_plugins.imagej_launcher -import os.{Path, up} - -import scala.scalanative.unsafe -import scala.scalanative.unsafe.* +import scala.scalanative.unsafe.extern 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 diff --git a/src/main/scala/ij_plugins/imagej_launcher/Updater.scala b/src/main/scala/ij_plugins/imagej_launcher/Updater.scala index c855f82..d73e70c 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Updater.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Updater.scala @@ -1,11 +1,6 @@ -/* - * 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 +import os.{Path, RelPath} import scala.util.control.NonFatal @@ -18,7 +13,7 @@ object Updater: * @param logger configured logger * @return Number of files processed or an error message. */ - def update(ijDir: Path, dryRun: Boolean)(using logger: Logger): Either[String, Long] = + def update(ijDir: Path, dryRun: Boolean, logger: Logger): Either[String, Long] = try val updateDir = ijDir / "update" // Count used only for debug info @@ -28,7 +23,8 @@ object Updater: os.walk(updateDir) .filter(os.isFile) .foreach: src => - val dst = ijDir / src.relativeTo(updateDir) +// val relativeDir = src.relativeTo(updateDir) + val dst = ijDir / relativeTo(src, updateDir) if os.size(src) == 0 then logger.debug(s"remove: $dst") if !dryRun then os.remove(dst) @@ -39,7 +35,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) + if !dryRun then deleteEmptyDirs(updateDir, logger) Right(count) else logger.info("No update found") @@ -49,11 +45,30 @@ object Updater: ex.printStackTrace() Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}") - private def deleteEmptyDirs(dir: Path)(using logger: Logger): Unit = + 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 = logger.debug(s"Cleaning directory: $dir") os.list(dir) .filter(os.isDir) - .foreach(p => deleteEmptyDirs(p)) + .foreach(p => deleteEmptyDirs(p, logger)) if os.list(dir).isEmpty then logger.debug(s"Removing empty dir: $dir") diff --git a/src/test/scala/ij_plugins/imagej_launcher/IJConfigFileTest.scala b/src/test/scala/ij_plugins/imagej_launcher/IJConfigFileTest.scala deleted file mode 100644 index f0260fe..0000000 --- a/src/test/scala/ij_plugins/imagej_launcher/IJConfigFileTest.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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") diff --git a/src/test/scala/ij_plugins/imagej_launcher/PathSpec.scala b/src/test/scala/ij_plugins/imagej_launcher/PathSpec.scala deleted file mode 100644 index 36845bb..0000000 --- a/src/test/scala/ij_plugins/imagej_launcher/PathSpec.scala +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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") - } diff --git a/src/test/scala/ij_plugins/imagej_launcher/UpdaterDemo.scala b/src/test/scala/ij_plugins/imagej_launcher/UpdaterDemo.scala index 49f674a..6e7f24a 100644 --- a/src/test/scala/ij_plugins/imagej_launcher/UpdaterDemo.scala +++ b/src/test/scala/ij_plugins/imagej_launcher/UpdaterDemo.scala @@ -4,7 +4,6 @@ import os.Path @main def updaterDemo(ijDir: String): Unit = - given logger: Logger = new Logger(Logger.Level.Debug) - Updater.update(Path(ijDir), dryRun = false) match + Updater.update(Path(ijDir), dryRun = false, new Logger(Logger.Level.Debug)) match case Right(count) => println(s"Processed $count files") case Left(error) => println(s"Failed with error: $error") diff --git a/test/data/config_invalid/ImageJ.cfg b/test/data/config_invalid/ImageJ.cfg deleted file mode 100644 index 9fc162e..0000000 --- a/test/data/config_invalid/ImageJ.cfg +++ /dev/null @@ -1,4 +0,0 @@ -# ImageJ startup properties -maxheap.mb = 1024m -jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc -legacy.mode = false \ No newline at end of file diff --git a/test/data/config_valid/ImageJ.cfg b/test/data/config_valid/ImageJ.cfg deleted file mode 100644 index 5a40166..0000000 --- a/test/data/config_valid/ImageJ.cfg +++ /dev/null @@ -1,4 +0,0 @@ -# ImageJ startup properties -maxheap.mb = 1024 -jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc -legacy.mode = false \ No newline at end of file