Compare commits

...

41 commits

Author SHA1 Message Date
Jarek Sacha
d6d8daca7b Update dependencies 2024-09-17 18:42:27 -04:00
Jarek Sacha
bbff825c9b Merge branch 'local/scala-native-0.5.0' 2023-09-26 21:53:31 -04:00
Jarek Sacha
6998af7cec Bump scalatest to 3.2.17 2023-09-26 21:29:04 -04:00
Jarek Sacha
1e3797fe19 Bump Scala to 3.3.1 2023-09-26 21:28:32 -04:00
Jarek Sacha
1003fd308d Bump ScalaFMT to 3.7.14 2023-09-26 21:27:45 -04:00
Jarek Sacha
e07360ec65 Update scala-native to 0.4.15 2023-09-26 21:27:14 -04:00
Jarek Sacha
abcc138ea0 Bump SBT to 1.9.6 2023-09-26 21:26:26 -04:00
Jarek Sacha
0ca11d6645
Add troubleshooting hints for macOS 2023-09-25 23:27:02 -04:00
Jarek Sacha
c8dec57570
Bump SBT to 1.9.4 2023-08-25 23:12:12 -04:00
Jarek Sacha
79ecae6091 Mark next development version: 0.2.0.1-SNAPSHOT 2023-08-20 22:05:35 -04:00
Jarek Sacha
c2043dc24c Merge branch 'release' 2023-08-20 22:03:21 -04:00
Jarek Sacha
4babd726f8 Update Windows installation instructions 2023-08-20 14:00:58 -04:00
Jarek Sacha
cbb9829f8e
Tweak macOS installation instruction 2023-08-19 20:57:39 -04:00
Jarek Sacha
0bca3977ad
Bump ScalaFMT to 3.7.12 2023-08-19 20:56:03 -04:00
Jarek Sacha
be57049c49
Update SBT to 1.9.3 2023-08-19 20:55:22 -04:00
Jarek Sacha
d842833e34
Ignore .DS_Store files 2023-06-11 23:02:02 -04:00
Jarek Sacha
da5ffe2701
ReadMe: add info about ~/.ijp_imagej_launcher.log 2023-06-11 22:59:53 -04:00
Jarek Sacha
8f158a0fe7
ReadMe: Update/simplify installation instructions for macOS 2023-06-11 22:59:19 -04:00
Jarek Sacha
bb18354923
Add release notes for v.0.2.0 2023-06-11 22:06:10 -04:00
Jarek Sacha
ad54989711
Mark release 0.2.0 2023-06-11 22:05:43 -04:00
Jarek Sacha
4e3fbe32a8
Merge branch 'master' into release 2023-06-11 21:32:48 -04:00
Jarek Sacha
8a417ceedb
Ensure that SBT project version and application version are in sync 2023-06-10 09:51:17 -04:00
Jarek Sacha
9a0cb694f7
Better inference of ImageJ directory on macOS - consider launcher being in subdirectory "Contents/MacOS" #7 2023-06-10 09:06:17 -04:00
Jarek Sacha
95638a462a
Log command line arguments
Effectively, at this point this is only sent to log file
2023-06-10 08:47:22 -04:00
Jarek Sacha
374a162f29
Remove unused imports 2023-06-07 20:32:06 -04:00
Jarek Sacha
11b248fc1f
Log session to ~/.ijp_imagej_launcher.log #6 2023-06-07 20:25:56 -04:00
Jarek Sacha
ce0c49c515
Update to Scala Native 0.4.14 2023-06-07 20:24:16 -04:00
Jarek Sacha
f2bb93871f
Add compiler warnings to help with the coding style 2023-06-07 18:30:38 -04:00
Jarek Sacha
eb8b6a9a17
Refactor: using given logger 2023-06-05 21:10:39 -04:00
Jarek Sacha
faf6a879e5
[WIP] Read ImageJ.cfg #3 2023-06-05 21:02:57 -04:00
Jarek Sacha
60ad1b1816
Correct type, remove extra single quote character 2023-06-05 18:54:04 -04:00
Jarek Sacha
8718c9310c
Remove workaround for Scala Native issue #3293, it was resolved in v.0.4.13
See https://github.com/scala-native/scala-native/issues/3293
2023-06-05 18:52:53 -04:00
Jarek Sacha
c30b853f64
Add test for Scala Native #3293 that should be resolved in v.0.4.13 2023-06-05 18:51:09 -04:00
Jarek Sacha
47674de560
Update to Scala Native 0.4.13 2023-06-05 18:42:01 -04:00
Jarek Sacha
87882dc6e3
Fixed: Use launcher location to infer ij-dir, similar to path in argv[0] #5 2023-06-04 18:25:59 -04:00
Jarek Sacha
1d5b8887a3
Readme: add info on setting executable permissions 2023-06-03 09:36:05 -04:00
Jarek Sacha
1f2c915159
Code style: add comment with plugin site 2023-06-02 17:54:09 -04:00
Jarek Sacha
231540bce5
Bump SBT to 1.9.0 2023-06-02 17:53:15 -04:00
Jarek Sacha
63b3ad6f52
Readme: Add info about Java 11+ focus 2023-06-02 17:52:42 -04:00
Jarek Sacha
e2b6e2d550
Mark next development version 2023-06-02 00:01:13 -04:00
Jarek Sacha
1312fa24b1
Correct typo 2023-06-01 23:59:24 -04:00
23 changed files with 489 additions and 109 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
.idea/ .idea/
target/ target/
.DS_Store

View file

@ -1,4 +1,4 @@
version = 3.7.4 version = 3.8.3
runner.dialect = scala3 runner.dialect = scala3

View file

@ -15,6 +15,8 @@ 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 Mac OS X Arm64](#installing-fiji-on-mac-os-x-arm64)
* [Installing Fiji on Windows x64](#installing-fiji-on-windows-x64) * [Installing Fiji on Windows x64](#installing-fiji-on-windows-x64)
* [Troubleshooting](#troubleshooting) * [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) * [Developer Setup](#developer-setup)
<!-- TOC --> <!-- TOC -->
@ -33,7 +35,10 @@ the logic flow is too complex to correct without a significant rewrite.
Features 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 * Provides native executable for various OS/Hardware systems
- Windows - Windows
- Mac OS X Arm64 (Apple Silicon) - Mac OS X Arm64 (Apple Silicon)
@ -48,7 +53,7 @@ Features
- Search ImageJ directory for available Java executables - 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 the amount of memory used by JVM based on total system memory use 75% of the max
* Determines available `imagej-launcher*.jar` * 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 Full List of Command Line Options
--------------------------------- ---------------------------------
@ -77,7 +82,7 @@ This example will show how to:
**1. Download FIJI without JRE** **1. Download FIJI without JRE**
Go to https://fiji.sc/ and select "Download the no-JRE version". Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
That should get file called `fiji-nojre.zip` That should get file called `fiji-nojre.zip`
**2. Unzip the `fiji-nojre.zip` in a folder of choice** **2. Unzip the `fiji-nojre.zip` in a folder of choice**
@ -95,41 +100,49 @@ Inside the `Fiji.app` folder create a new folder called `java`.
In browser open https://adoptium.net/temurin/releases/ In browser open https://adoptium.net/temurin/releases/
Select: Select:
* operating system: `macOS` * Operating System: `macOS`
* architecture: `aarch64` also known as Apple Silicon or Arm64 * Architecture: `aarch64` also known as Apple Silicon or Arm64
* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation) * 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) * 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. Click on `tar.gz` button to download and save to the `java` folder you created earlier.
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.tar.gz`. You should have file like `OpenJDK11U-jre_aarch64_mac_hotspot_11.0.20_8.tar.gz`.
**5. Uncompress into the `java` folder** **5. Uncompress into the `Fiji.app/java` folder**
That will create folder like `jdk-11.0.19+7-jre`. That will create folder like `jdk-11.0.20+8-jre`.
This is the Java VM that IJP ImageJ Launcher will use to start Fiji. 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** **6. Download the IJP ImageJ Launcher and uncompress**
Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-macosx-arm64" Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip"
and "IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command", save them to the `Fiji.app` folder.
The "*.command" file is a helper that can be used to launch Fiji without using command prompt. Uncompress "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip".
Future versions of the IJP Launcher, after v.0.1.0, may eliminate the need for using this file. Inside you will get `ImageJ-macosx`.
**7. Start ImageJ** **7. Add to Fiji.app**
In the `Fiji.app` folder double-click on `IJP-ImageL-Launcher-0.1.0-macosx-arm64.command` file (note the extension "* Inside `Fiji.app` locate folder `Contents/MacOS`.
.command")
That should start Fiji.
You may need to open Settings and allow the IJP ImageJ Launcher to run.
You can also create an alis on the Desktop to avoid navigating to the `Fiji.app` folder each time. Copy `ImageJ-macosx` to the `Contents/MacOS` folder, replacing `ImageJ-macosx` that was there.
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 **8. Move Fiji.app to the Application folder**
on the Desktop.
Now you can double-click on the new alias on the Desktop to start Fiji. At this point you can move the `Fiji.app` folder to the Applications folder and use is as a regular msOS application.
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. **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
If you have problems installing, please report in [Discussions] or [Image.sc Forum] If you have problems installing, please report in [Discussions] or [Image.sc Forum]
@ -143,7 +156,7 @@ This example will show how to:
**1. Download FIJI without JRE** **1. Download FIJI without JRE**
Go to https://fiji.sc/ and select "Download the no-JRE version". Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
That should get file called `fiji-nojre.zip` That should get file called `fiji-nojre.zip`
**2. Unzip the `fiji-nojre.zip` in a folder of choice** **2. Unzip the `fiji-nojre.zip` in a folder of choice**
@ -160,31 +173,31 @@ Inside the `Fiji.app` folder create a new folder called `java`.
In browser open https://adoptium.net/temurin/releases/ In browser open https://adoptium.net/temurin/releases/
Select: Select:
* operating system: `Windows` * Operating System: `Windows`
* architecture: `x64` also known as Apple Silicon or Arm64 * Architecture: `x64` also known as Apple Silicon or Arm64
* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation) * 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) * 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. 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.19_7.zip`. You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.20_8.zip`.
**5. Uncompress into the `java` folder** **5. Uncompress into the `Fiji.app/java` folder**
That will create folder like `jdk-11.0.19+7-jre`. That will create folder like `jdk-11.0.20+8-jre`.
This is the Java VM that IJP ImageJ Launcher will use to start Fiji. 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** **6. Download the IJP ImageJ Launcher to the Fiji.app directory**
Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-windows_x64.exe", save it to the `Fiji.app` folder. Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-windows_x64.exe", save it to the `Fiji.app` folder.
**7. Start ImageJ** **7. Start ImageJ**
In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.1.0-windows_x64.exe`. In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.2.0-windows_x64.exe`.
That should start Fiji. That should start Fiji.
You can also create a shortcut on the Desktop to avoid navigating to the `Fiji.app` folder each time. 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.1.0-windows_x64.exe` and drag it to the Desktop. **_Left_**-click on the `IJP-ImageJ-Launcher-0.2.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". 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. Now you can double-click on the new shortcut on the Desktop to start Fiji.
@ -194,6 +207,13 @@ If you have problems installing, please report in [Discussions] or [Image.sc For
### Troubleshooting ### 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 You can start the IJP Image Launcher from the terminal and see diagnostic printouts that may help troubleshoot potential
issues. issues.

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View file

@ -1,6 +1,6 @@
scalaVersion := "3.3.0" scalaVersion := "3.3.3"
//name := "IJP-ImageJ-Launcher" //name := "IJP-ImageJ-Launcher"
version := "0.1.0" version := "0.2.0.1-SNAPSHOT"
versionScheme := Some("early-semver") versionScheme := Some("early-semver")
organization := "net.sf.ij-plugins" organization := "net.sf.ij-plugins"
homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL) homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL)
@ -17,13 +17,25 @@ logLevel := Level.Info
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.github.scopt" %%% "scopt" % "4.1.0", "com.github.scopt" %%% "scopt" % "4.1.0",
"com.lihaoyi" %%% "os-lib" % "0.9.1" "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"
) )
Compile / run / mainClass := Some("ij_plugins.imagej_launcher.Main") Compile / run / mainClass := Some("ij_plugins.imagej_launcher.Main")
// import to add Scala Native options // import to add Scala Native options
import scala.scalanative.build._ import scala.scalanative.build.*
// defaults set with common options shown // defaults set with common options shown
nativeConfig ~= { c => nativeConfig ~= { c =>
@ -36,3 +48,8 @@ nativeConfig ~= { c =>
//nativeConfig ~= { c => //nativeConfig ~= { c =>
// c.withCompileOptions(c.compileOptions ++ Seq("-v")) // 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 #!/bin/bash
DIR=$(cd "$(dirname "$0")" && pwd -P) DIR=$(cd "$(dirname "$0")" && pwd -P)
echo "$DIR" 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"

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

@ -0,0 +1,8 @@
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

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.10.2

2
project/buildinfo.sbt Normal file
View file

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

View file

@ -1 +1,3 @@
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") // https://github.com/scala-native/scala-native
resolvers ++= Resolver.sonatypeOssRepos("snapshots")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")

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 package ij_plugins.imagej_launcher
import ij_plugins.imagej_launcher.IJDir.jarsDirName
import ij_plugins.imagej_launcher.Launcher.javaExeFileName import ij_plugins.imagej_launcher.Launcher.javaExeFileName
import ij_plugins.imagej_launcher.Main.Config import ij_plugins.imagej_launcher.Main.Config
import os.Path import os.Path
@ -12,9 +13,7 @@ import os.Path
import java.io.File import java.io.File
import java.lang.ProcessBuilder.Redirect import java.lang.ProcessBuilder.Redirect
class Launcher(logger: Logger): class Launcher(using logger: Logger):
private val jarsDirName = "jars"
def run(config: Config): Unit = def run(config: Config): Unit =
prepareLaunch(config) match prepareLaunch(config) match
@ -29,33 +28,16 @@ class Launcher(logger: Logger):
private def prepareLaunch(config: Config): Either[String, Seq[String]] = private def prepareLaunch(config: Config): Either[String, Seq[String]] =
for for
ijDir <- locateIJDir(config) ijDir <- IJDir.locate(config)
_ <- Updater.update(Path(ijDir), config.dryRun, logger) _ <- Updater.update(ijDir, config.dryRun)
launcherJar <- findImageJLauncherJar(ijDir) ijConfig <- IJConfigFile.readFromDir(ijDir)
javaExe <- locateJavaExecutable(config, ijDir) launcherJar <- findImageJLauncherJar(ijDir.toIO)
javaExe <- locateJavaExecutable(config, ijDir.toIO)
systemType <- determineSystemType() systemType <- determineSystemType()
yield yield
val maxMemoryMB = determineMaxMemoryMB() val maxMemoryMB = determineMaxMemoryMB()
logger.info(s"Max memory to use: ${maxMemoryMB}MB") logger.info(s"Max memory to use: ${maxMemoryMB}MB")
buildCommandLine(ijDir, javaExe, launcherJar, systemType, maxMemoryMB) buildCommandLine(ijDir.toIO, 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] = private def findImageJLauncherJar(ijDir: File): Either[String, File] =
logger.debug("Looking for 'imagej-launcher*.jar'") logger.debug("Looking for 'imagej-launcher*.jar'")
@ -197,6 +179,7 @@ class Launcher(logger: Logger):
"plugins", "plugins",
"net.imagej.Main" "net.imagej.Main"
) )
end buildCommandLine
private def launch(command: Seq[String]): Unit = private def launch(command: Seq[String]): Unit =
logger.debug("launchImageJ ...") logger.debug("launchImageJ ...")

View file

@ -5,20 +5,60 @@
package ij_plugins.imagej_launcher 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): class Logger(val level: Level = Level.Info):
def debug(msg: String): Unit = pprint(Level.Debug, msg) def debug(msg: String): Unit = pprint(Level.Debug, msg)
def info(msg: String): Unit = pprint(Level.Info, msg) def info(msg: String): Unit = pprint(Level.Info, msg)
def error(msg: String): Unit = pprint(Level.Error, msg) def error(msg: String): Unit = pprint(Level.Error, msg)
private def pprint(l: Level, message: String): Unit = 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") if l.level <= level.level then println(f"${l.name}%-5s: $message")
logToFile(m)
object Logger: 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): enum Level(val level: Int, val name: String):
case Off extends Level(0, "OFF") case Off extends Level(0, "OFF")
case Error extends Level(200, "ERROR") case Error extends Level(200, "ERROR")
case Info extends Level(400, "INFO") case Info extends Level(400, "INFO")
case Debug extends Level(500, "DEBUG") case Debug extends Level(500, "DEBUG")
case All extends Level(Int.MaxValue, "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 package ij_plugins.imagej_launcher
import scopt.{DefaultOEffectSetup, OParser} import scopt.OParser
import java.io.File import java.io.File
object Main: object Main:
private var logger = new Logger() private var logger = new Logger()
private val AppName = "ijp-imagej-launcher" private val AppName = "IJP-ImageJ-Launcher"
// private val AppVersion = s"${Version.version} [${Version.buildTimeStr}]" private val AppVersion = BuildInfo.version
private val AppVersion = s"0.1.0"
private val VersionMessage = s"v.$AppVersion" private val VersionMessage = s"v.$AppVersion"
private val AppDescription = private val AppDescription =
"""Native launcher for ImageJ2 """Native launcher for ImageJ2
@ -29,6 +28,7 @@ object Main:
case None => case None =>
private def parseCommandLine(args: Array[String]): Option[Config] = private def parseCommandLine(args: Array[String]): Option[Config] =
logger.debug("Command line: " + args.map("'" + _ + "'").mkString(", "))
val builder = OParser.builder[Config] val builder = OParser.builder[Config]
val parser1 = val parser1 =
import builder.* import builder.*
@ -69,7 +69,7 @@ object Main:
private def setupLogger(logLevel: Logger.Level): Unit = logger = new Logger(logLevel) 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( case class Config(
logLevel: Logger.Level = Logger.Level.Error, logLevel: Logger.Level = Logger.Level.Error,

View file

@ -1,9 +1,29 @@
package ij_plugins.imagej_launcher package ij_plugins.imagej_launcher
import scala.scalanative.unsafe.extern import os.{Path, up}
import scala.scalanative.unsafe
import scala.scalanative.unsafe.*
object Native: 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 @extern
object mem: object mem:
def determineTotalSystemMemory(): Long = extern 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 package ij_plugins.imagej_launcher
import os.{Path, RelPath} import os.Path
import scala.util.control.NonFatal import scala.util.control.NonFatal
@ -13,7 +18,7 @@ object Updater:
* @param logger configured logger * @param logger configured logger
* @return Number of files processed or an error message. * @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 try
val updateDir = ijDir / "update" val updateDir = ijDir / "update"
// Count used only for debug info // Count used only for debug info
@ -23,8 +28,7 @@ object Updater:
os.walk(updateDir) os.walk(updateDir)
.filter(os.isFile) .filter(os.isFile)
.foreach: src => .foreach: src =>
// val relativeDir = src.relativeTo(updateDir) val dst = ijDir / src.relativeTo(updateDir)
val dst = ijDir / relativeTo(src, updateDir)
if os.size(src) == 0 then if os.size(src) == 0 then
logger.debug(s"remove: $dst") logger.debug(s"remove: $dst")
if !dryRun then os.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) if !dryRun then os.move(src, dst, replaceExisting = true, createFolders = true)
count += 1 count += 1
logger.debug(s"Delete update directory: $updateDir") logger.debug(s"Delete update directory: $updateDir")
if !dryRun then deleteEmptyDirs(updateDir, logger) if !dryRun then deleteEmptyDirs(updateDir)
Right(count) Right(count)
else else
logger.info("No update found") logger.info("No update found")
@ -45,30 +49,11 @@ object Updater:
ex.printStackTrace() ex.printStackTrace()
Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}") Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}")
private def relativeTo(src: Path, base: Path): RelPath = private def deleteEmptyDirs(dir: Path)(using logger: Logger): Unit =
// 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") logger.debug(s"Cleaning directory: $dir")
os.list(dir) os.list(dir)
.filter(os.isDir) .filter(os.isDir)
.foreach(p => deleteEmptyDirs(p, logger)) .foreach(p => deleteEmptyDirs(p))
if os.list(dir).isEmpty then if os.list(dir).isEmpty then
logger.debug(s"Removing empty dir: $dir") 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 @main
def updaterDemo(ijDir: String): Unit = 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 Right(count) => println(s"Processed $count files")
case Left(error) => println(s"Failed with error: $error") 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