From 194871c04b96c434077ab5afffb0f310dd6e4342 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 18 Oct 2023 10:35:36 +0200 Subject: [PATCH 001/120] Begin working on the conda environment builder --- pom.xml | 10 + src/main/java/org/apposed/appose/Builder.java | 24 +- src/main/java/org/apposed/appose/Conda.java | 599 ++++++++++++++++++ .../java/org/apposed/appose/ApposeTest.java | 8 + 4 files changed, 639 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/apposed/appose/Conda.java diff --git a/pom.xml b/pom.xml index 0467583..cea0ebe 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,16 @@ jna-platform + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + org.junit.jupiter diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 18106f4..c6e6e03 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -30,17 +30,37 @@ package org.apposed.appose; import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; public class Builder { public Environment build() { + String base = baseDir.getPath(); + boolean useSystemPath = systemPath; + // TODO Build the thing!~ // Hash the state to make a base directory name. // - Construct conda environment from condaEnvironmentYaml. // - Download and unpack JVM of the given vendor+version. // - Populate ${baseDirectory}/jars with Maven artifacts? - String base = baseDir.getPath(); - boolean useSystemPath = systemPath; + + try { + String minicondaBase = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "miniconda").toString(); + Conda conda = new Conda(minicondaBase); + String envName = "appose"; + if (conda.getEnvironmentNames().contains( envName )) { + // TODO: Should we update it? For now, we just use it. + } + else { + conda.create(envName, "-f", condaEnvironmentYaml.getAbsolutePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return new Environment() { @Override public String base() { return base; } @Override public boolean useSystemPath() { return useSystemPath; } diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java new file mode 100644 index 0000000..29e7c73 --- /dev/null +++ b/src/main/java/org/apposed/appose/Conda.java @@ -0,0 +1,599 @@ +/******************************************************************************* + * Copyright (C) 2021, Ko Sugawara + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package org.apposed.appose; + +import org.apache.commons.lang3.SystemUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Conda environment manager, implemented by delegating to micromamba. + * + * @author Ko Sugawara + * @author Curtis Rueden + */ +public class Conda { + + private final static int TIMEOUT_MILLIS = 10 * 1000; + + private final static String MICROMAMBA_URL = + "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; + + private static String microMambaPlatform() { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Windows")) osName = "Windows"; + String osArch = System.getProperty("os.arch"); + switch (osName + "|" + osArch) { + case "Linux|amd64": return "linux-64"; + case "Linux|aarch64": return "linux-aarch64"; + case "Linux|ppc64le": return "linux-ppc64le"; + case "Mac OS X|x86_64": return "osx-64"; + case "Mac OS X|aarch64": return "osx-arm64"; + case "Windows|amd64": return "win-64"; + default: return null; + } + } + + /** + * Returns a {@link ProcessBuilder} with the working directory specified in the + * constructor. + * + * @param isInheritIO + * Sets the source and destination for subprocess standard I/O to be + * the same as those of the current Java process. + * @return The {@link ProcessBuilder} with the working directory specified in + * the constructor. + */ + private ProcessBuilder getBuilder( final boolean isInheritIO ) + { + final ProcessBuilder builder = new ProcessBuilder().directory( new File( rootdir ) ); + if ( isInheritIO ) + builder.inheritIO(); + return builder; + } + + /** + * Create a new Conda object. The root dir for Conda installation can be + * specified as {@code String}. If there is no directory found at the specified + * path, Miniconda will be automatically installed in the path. It is expected + * that the Conda installation has executable commands as shown below: + * + *
+	 * CONDA_ROOT
+	 * ├── condabin
+	 * │   ├── conda(.bat)
+	 * │   ... 
+	 * ├── envs
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
+	 * 
+ * + * @param rootdir + * The root dir for Conda installation. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public Conda( final String rootdir ) throws IOException, InterruptedException + { + if ( Files.notExists( Paths.get( rootdir ) ) ) + { + String downloadUrl = null; + if ( SystemUtils.IS_OS_WINDOWS ) + downloadUrl = DOWNLOAD_URL_WIN; + else if ( SystemUtils.IS_OS_LINUX ) + downloadUrl = DOWNLOAD_URL_LINUX; + else if ( SystemUtils.IS_OS_MAC && System.getProperty( "os.arch" ).equals( "aarch64" ) ) + downloadUrl = DOWNLOAD_URL_MAC_M1; + else if ( SystemUtils.IS_OS_MAC ) + downloadUrl = DOWNLOAD_URL_MAC; + else + throw new UnsupportedOperationException(); + + final File tempFile = File.createTempFile( "miniconda", SystemUtils.IS_OS_WINDOWS ? ".exe" : ".sh" ); + tempFile.deleteOnExit(); + FileUtils.copyURLToFile( + new URL( downloadUrl ), + tempFile, + TIMEOUT_MILLIS, + TIMEOUT_MILLIS ); + final List< String > cmd = getBaseCommand(); + + if ( SystemUtils.IS_OS_WINDOWS ) + cmd.addAll( Arrays.asList( "start", "/wait", "\"\"", tempFile.getAbsolutePath(), "/InstallationType=JustMe", "/AddToPath=0", "/RegisterPython=0", "/S", "/D=" + rootdir ) ); + else + cmd.addAll( Arrays.asList( "bash", tempFile.getAbsolutePath(), "-b", "-p", rootdir ) ); + if ( new ProcessBuilder().inheritIO().command( cmd ).start().waitFor() != 0 ) + throw new RuntimeException(); + } + this.rootdir = rootdir; + + // The following command will throw an exception if Conda does not work as + // expected. + final List< String > cmd = getBaseCommand(); + cmd.addAll( Arrays.asList( condaCommand, "-V" ) ); + if ( getBuilder( false ).command( cmd ).start().waitFor() != 0 ) + throw new RuntimeException(); + } + + /** + * Run {@code conda update} in the activated environment. A list of packages to + * be updated and extra parameters can be specified as {@code args}. + * + * @param args + * The list of packages to be updated and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void update( final String... args ) throws IOException, InterruptedException + { + updateIn( envName, args ); + } + + /** + * Run {@code conda update} in the specified environment. A list of packages to + * update and extra parameters can be specified as {@code args}. + * + * @param envName + * The environment name to be used for the update command. + * @param args + * The list of packages to be updated and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException + { + final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-y", "-n", envName ) ); + cmd.addAll( Arrays.asList( args ) ); + runConda( cmd.stream().toArray( String[]::new ) ); + } + + /** + * Run {@code conda create} to create an empty conda environment. + * + * @param envName + * The environment name to be created. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName ) throws IOException, InterruptedException + { + create( envName, false ); + } + + /** + * Run {@code conda create} to create an empty conda environment. + * + * @param envName + * The environment name to be created. + * @param isForceCreation + * Force creation of the environment if {@code true}. If this value + * is {@code false} and an environment with the specified name + * already exists, throw an {@link EnvironmentExistsException}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName, final boolean isForceCreation ) throws IOException, InterruptedException + { + if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) + throw new EnvironmentExistsException(); + runConda( "create", "-y", "-n", envName ); + } + + /** + * Run {@code conda create} to create a new conda environment with a list of + * specified packages. + * + * @param envName + * The environment name to be created. + * @param args + * The list of packages to be installed on environment creation and + * extra parameters as {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName, final String... args ) throws IOException, InterruptedException + { + create( envName, false, args ); + } + + /** + * Run {@code conda create} to create a new conda environment with a list of + * specified packages. + * + * @param envName + * The environment name to be created. + * @param isForceCreation + * Force creation of the environment if {@code true}. If this value + * is {@code false} and an environment with the specified name + * already exists, throw an {@link EnvironmentExistsException}. + * @param args + * The list of packages to be installed on environment creation and + * extra parameters as {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException + { + if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) + throw new EnvironmentExistsException(); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-n", envName ) ); + cmd.addAll( Arrays.asList( args ) ); + runConda( cmd.stream().toArray( String[]::new ) ); + } + + /** + * This method works as if the user runs {@code conda activate envName}. This + * method internally calls {@link Conda#setEnvName(String)}. + * + * @param envName + * The environment name to be activated. + * @throws IOException + * If an I/O error occurs. + */ + public void activate( final String envName ) throws IOException + { + if ( getEnvironmentNames().contains( envName ) ) + setEnvName( envName ); + else + throw new IllegalArgumentException( "environment: " + envName + " not found." ); + } + + /** + * This method works as if the user runs {@code conda deactivate}. This method + * internally sets the {@code envName} to {@code base}. + */ + public void deactivate() + { + setEnvName( DEFAULT_ENVIRONMENT_NAME ); + } + + /** + * This method is used by {@code Conda#activate(String)} and + * {@code Conda#deactivate()}. This method is kept private since it is not + * expected to call this method directory. + * + * @param envName + * The environment name to be set. + */ + private void setEnvName( final String envName ) + { + this.envName = envName; + } + + /** + * Returns the active environment name. + * + * @return The active environment name. + * + */ + public String getEnvName() + { + return envName; + } + + /** + * Run {@code conda install} in the activated environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param args + * The list of packages to be installed and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void install( final String... args ) throws IOException, InterruptedException + { + installIn( envName, args ); + } + + /** + * Run {@code conda install} in the specified environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param envName + * The environment name to be used for the install command. + * @param args + * The list of packages to be installed and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void installIn( final String envName, final String... args ) throws IOException, InterruptedException + { + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-n", envName ) ); + cmd.addAll( Arrays.asList( args ) ); + runConda( cmd.stream().toArray( String[]::new ) ); + } + + /** + * Run {@code pip install} in the activated environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param args + * The list of packages to be installed and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void pipInstall( final String... args ) throws IOException, InterruptedException + { + pipInstallIn( envName, args ); + } + + /** + * Run {@code pip install} in the specified environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param envName + * The environment name to be used for the install command. + * @param args + * The list of packages to be installed and extra parameters as + * {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void pipInstallIn( final String envName, final String... args ) throws IOException, InterruptedException + { + final List< String > cmd = new ArrayList<>( Arrays.asList( "-m", "pip", "install" ) ); + cmd.addAll( Arrays.asList( args ) ); + runPythonIn( envName, cmd.stream().toArray( String[]::new ) ); + } + + /** + * Run a Python command in the activated environment. This method automatically + * sets environment variables associated with the activated environment. In + * Windows, this method also sets the {@code PATH} environment variable so that + * the specified environment runs as expected. + * + * @param args + * One or more arguments for the Python command. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void runPython( final String... args ) throws IOException, InterruptedException + { + runPythonIn( envName, args ); + } + + /** + * Run a Python command in the specified environment. This method automatically + * sets environment variables associated with the specified environment. In + * Windows, this method also sets the {@code PATH} environment variable so that + * the specified environment runs as expected. + * + * @param envName + * The environment name used to run the Python command. + * @param args + * One or more arguments for the Python command. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void runPythonIn( final String envName, final String... args ) throws IOException, InterruptedException + { + final List< String > cmd = getBaseCommand(); + if ( envName.equals( DEFAULT_ENVIRONMENT_NAME ) ) + cmd.add( pythonCommand ); + else + cmd.add( Paths.get( "envs", envName, pythonCommand ).toString() ); + cmd.addAll( Arrays.asList( args ) ); + final ProcessBuilder builder = getBuilder( true ); + if ( SystemUtils.IS_OS_WINDOWS ) + { + final Map< String, String > envs = builder.environment(); + final String envDir = Paths.get( rootdir, "envs", envName ).toString(); + envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library", "Bin" ).toString() + ";" + envs.get( "Path" ) ); + } + builder.environment().putAll( getEnvironmentVariables( envName ) ); + if ( builder.command( cmd ).start().waitFor() != 0 ) + throw new RuntimeException(); + } + + /** + * Returns Conda version as a {@code String}. + * + * @return The Conda version as a {@code String}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public String getVersion() throws IOException, InterruptedException + { + final List< String > cmd = getBaseCommand(); + cmd.addAll( Arrays.asList( condaCommand, "-V" ) ); + final Process process = getBuilder( false ).command( cmd ).start(); + if ( process.waitFor() != 0 ) + throw new RuntimeException(); + return new BufferedReader( new InputStreamReader( process.getInputStream() ) ).readLine(); + } + + /** + * Run a Conda command with one or more arguments. + * + * @param args + * One or more arguments for the Conda command. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void runConda( final String... args ) throws RuntimeException, IOException, InterruptedException + { + final List< String > cmd = getBaseCommand(); + cmd.add( condaCommand ); + cmd.addAll( Arrays.asList( args ) ); + if ( getBuilder( true ).command( cmd ).start().waitFor() != 0 ) + throw new RuntimeException(); + } + + /** + * Returns environment variables associated with the activated environment as + * {@code Map< String, String >}. + * + * @return The environment variables as {@code Map< String, String >}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public Map< String, String > getEnvironmentVariables() throws IOException, InterruptedException + { + return getEnvironmentVariables( envName ); + } + + /** + * Returns environment variables associated with the specified environment as + * {@code Map< String, String >}. + * + * @param envName + * The environment name used to run the Python command. + * @return The environment variables as {@code Map< String, String >}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public Map< String, String > getEnvironmentVariables( final String envName ) throws IOException, InterruptedException + { + final List< String > cmd = getBaseCommand(); + cmd.addAll( Arrays.asList( condaCommand, "env", "config", "vars", "list", "-n", envName ) ); + final Process process = getBuilder( false ).command( cmd ).start(); + if ( process.waitFor() != 0 ) + throw new RuntimeException(); + final Map< String, String > map = new HashMap<>(); + try (final BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) )) + { + String line; + + while ( ( line = reader.readLine() ) != null ) + { + final String[] keyVal = line.split( " = " ); + map.put( keyVal[ 0 ], keyVal[ 1 ] ); + } + } + return map; + } + + /** + * Returns a list of the Conda environment names as {@code List< String >}. + * + * @return The list of the Conda environment names as {@code List< String >}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public List< String > getEnvironmentNames() throws IOException + { + final List< String > envs = new ArrayList<>( Arrays.asList( DEFAULT_ENVIRONMENT_NAME ) ); + envs.addAll( Files.list( Paths.get( rootdir, "envs" ) ) + .map( p -> p.getFileName().toString() ) + .filter( p -> !p.startsWith( "." ) ) + .collect( Collectors.toList() ) ); + return envs; + } + +} diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 5770cc4..26a8159 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -86,6 +86,14 @@ public void testPython() throws IOException, InterruptedException { } } + public void testConda() { + Environment env = Appose.conda("appose-environment.yml").build(); + try (Service service = env.python()) { + service.debug(System.err::println); + executeAndAssert(service, "import cowsay; "); + } + } + @Test public void testServiceStartupFailure() throws IOException { Environment env = Appose.base("no-pythons-to-be-found-here").build(); From 673ffe4c1ac6f34e5eee9f07ba04116f8528351e Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 2 Nov 2023 20:56:34 +0100 Subject: [PATCH 002/120] test commit --- src/test/java/org/apposed/appose/ApposeTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 26a8159..6b5522b 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.fail; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -87,10 +88,13 @@ public void testPython() throws IOException, InterruptedException { } public void testConda() { - Environment env = Appose.conda("appose-environment.yml").build(); + Environment env = Appose.conda(new File("appose-environment.yml")).build(); try (Service service = env.python()) { service.debug(System.err::println); executeAndAssert(service, "import cowsay; "); + } catch (IOException | InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); } } From 9dda13dc0774814cfa38eeb9250395252019f4a7 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 2 Nov 2023 21:00:27 +0100 Subject: [PATCH 003/120] add code to be able to download micromamba --- src/main/java/org/apposed/appose/Conda.java | 116 ++++++++++++++++-- .../org/apposed/appose/CondaException.java | 96 +++++++++++++++ 2 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/apposed/appose/CondaException.java diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 29e7c73..62d8d73 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -26,7 +26,11 @@ ******************************************************************************/ package org.apposed.appose; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; +import org.apposed.appose.CondaException.EnvironmentExistsException; + +import groovy.json.JsonSlurper; import java.io.BufferedReader; import java.io.File; @@ -49,6 +53,16 @@ * @author Curtis Rueden */ public class Conda { + + final String pythonCommand = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; + + final String condaCommand = SystemUtils.IS_OS_WINDOWS ? "condabin\\conda.bat" : "condabin/conda"; + + private String envName = DEFAULT_ENVIRONMENT_NAME; + + private final String rootdir; + + public final static String DEFAULT_ENVIRONMENT_NAME = "base"; private final static int TIMEOUT_MILLIS = 10 * 1000; @@ -117,25 +131,20 @@ public Conda( final String rootdir ) throws IOException, InterruptedException { if ( Files.notExists( Paths.get( rootdir ) ) ) { - String downloadUrl = null; - if ( SystemUtils.IS_OS_WINDOWS ) - downloadUrl = DOWNLOAD_URL_WIN; - else if ( SystemUtils.IS_OS_LINUX ) - downloadUrl = DOWNLOAD_URL_LINUX; - else if ( SystemUtils.IS_OS_MAC && System.getProperty( "os.arch" ).equals( "aarch64" ) ) - downloadUrl = DOWNLOAD_URL_MAC_M1; - else if ( SystemUtils.IS_OS_MAC ) - downloadUrl = DOWNLOAD_URL_MAC; - else - throw new UnsupportedOperationException(); - final File tempFile = File.createTempFile( "miniconda", SystemUtils.IS_OS_WINDOWS ? ".exe" : ".sh" ); + final File tempFile = File.createTempFile( "miniconda", SystemUtils.IS_OS_WINDOWS ? ".tar.bz2" : ".sh" ); tempFile.deleteOnExit(); FileUtils.copyURLToFile( - new URL( downloadUrl ), + new URL( MICROMAMBA_URL ), tempFile, TIMEOUT_MILLIS, TIMEOUT_MILLIS ); + + String command = "tar xf " + tempFile.getAbsolutePath(); + // Setting up the ProcessBuilder to use PowerShell + ProcessBuilder processBuilder = new ProcessBuilder("powershell.exe", "-Command", command); + if ( processBuilder.inheritIO().start().waitFor() != 0 ) + throw new RuntimeException(); final List< String > cmd = getBaseCommand(); if ( SystemUtils.IS_OS_WINDOWS ) @@ -155,6 +164,22 @@ else if ( SystemUtils.IS_OS_MAC ) throw new RuntimeException(); } + /** + * Returns {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for + * Mac/Linux. + * + * @return {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for + * Mac/Linux. + * @throws IOException + */ + private List< String > getBaseCommand() + { + final List< String > cmd = new ArrayList<>(); + if ( SystemUtils.IS_OS_WINDOWS ) + cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); + return cmd; + } + /** * Run {@code conda update} in the activated environment. A list of packages to * be updated and extra parameters can be specified as {@code args}. @@ -197,6 +222,54 @@ public void updateIn( final String envName, final String... args ) throws IOExce runConda( cmd.stream().toArray( String[]::new ) ); } + /** + * Run {@code conda create} to create a conda environment defined by the input environment yaml file. + * + * @param envName + * The environment name to be created. + * @param envYaml + * The environment yaml file containing the information required to build it + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void createWithYaml( final String envName, final String envYaml ) throws IOException, InterruptedException + { + createWithYaml(envName, envYaml); + } + + /** + * Run {@code conda create} to create a conda environment defined by the input environment yaml file. + * + * @param envName + * The environment name to be created. + * @param envYaml + * The environment yaml file containing the information required to build it + * @param envName + * The environment name to be created. + * @param isForceCreation + * Force creation of the environment if {@code true}. If this value + * is {@code false} and an environment with the specified name + * already exists, throw an {@link EnvironmentExistsException}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation ) throws IOException, InterruptedException + { + if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) + throw new EnvironmentExistsException(); + runConda( "env", "create", "--prefix", + rootdir + File.separator + "envs", "--force", + "-n", envName, "--file", envYaml, "-y" ); + } + /** * Run {@code conda create} to create an empty conda environment. * @@ -595,5 +668,22 @@ public List< String > getEnvironmentNames() throws IOException .collect( Collectors.toList() ) ); return envs; } + + public static boolean checkDependenciesInEnv(String envDir, List dependencies) { + if (!(new File(envDir).isDirectory())) + return false; + Builder env = new Builder().conda(new File(envDir)); + // TODO run conda list -p /full/path/to/env + return false; + } + + public boolean checkEnvFromYamlExists(String envYaml) { + if (envYaml == null || new File(envYaml).isFile() == false + || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { + return false; + } + // TODO parse yaml without adding deps + return false; + } } diff --git a/src/main/java/org/apposed/appose/CondaException.java b/src/main/java/org/apposed/appose/CondaException.java new file mode 100644 index 0000000..632fc7b --- /dev/null +++ b/src/main/java/org/apposed/appose/CondaException.java @@ -0,0 +1,96 @@ +package org.apposed.appose; + +public class CondaException +{ + + public static class EnvironmentExistsException extends RuntimeException + { + private static final long serialVersionUID = -1625119813967214783L; + + /** + * Constructs a new exception with {@code null} as its detail message. The cause + * is not initialized, and may subsequently be initialized by a call to + * {@link #initCause}. + */ + public EnvironmentExistsException() + { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is + * not initialized, and may subsequently be initialized by a call to + * {@link #initCause}. + * + * @param msg + * the detail message. The detail message is saved for later + * retrieval by the {@link #getMessage()} method. + */ + public EnvironmentExistsException( String msg ) + { + super( msg ); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with {@code cause} is not + * automatically incorporated in this exception's detail message. + * + * @param message + * the detail message (which is saved for later retrieval by the + * {@link #getMessage()} method). + * @param cause + * the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @since 1.4 + */ + public EnvironmentExistsException( String message, Throwable cause ) + { + super( message, cause ); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains + * the class and detail message of cause). This constructor is useful + * for exceptions that are little more than wrappers for other throwables (for + * example, {@link java.security.PrivilegedActionException}). + * + * @param cause + * the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @since 1.4 + */ + public EnvironmentExistsException( Throwable cause ) + { + super( cause ); + } + + /** + * Constructs a new exception with the specified detail message, cause, + * suppression enabled or disabled, and writable stack trace enabled or + * disabled. + * + * @param message + * the detail message. + * @param cause + * the cause. (A {@code null} value is permitted, and indicates that + * the cause is nonexistent or unknown.) + * @param enableSuppression + * whether or not suppression is enabled or disabled + * @param writableStackTrace + * whether or not the stack trace should be writable + * @since 1.7 + */ + protected EnvironmentExistsException( String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace ) + { + super( message, cause, enableSuppression, writableStackTrace ); + } + } + +} From aef15793a25ec48b568501dbc19ac41795c822a5 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 2 Nov 2023 21:58:23 +0100 Subject: [PATCH 004/120] add utils to decompress mamba --- .../java/org/apposed/appose/Bzip2Utils.java | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/main/java/org/apposed/appose/Bzip2Utils.java diff --git a/src/main/java/org/apposed/appose/Bzip2Utils.java b/src/main/java/org/apposed/appose/Bzip2Utils.java new file mode 100644 index 0000000..6fbc29b --- /dev/null +++ b/src/main/java/org/apposed/appose/Bzip2Utils.java @@ -0,0 +1,285 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.apposed.appose; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.itadaki.bzip2.BZip2InputStream; + +/** + * Utility methods unzip bzip2 files + */ +public final class Bzip2Utils { + + final private static int BUFFER_SIZE = 1024 * 20; + + private Bzip2Utils() { + // Prevent instantiation of utility class. + } + + /** + * DEcompress a bzip2 file into a new file. + * The method is needed because Micromamba is distributed as a .tr.bz2 file and + * many distributions do not have tools readily available to extract the required files + * @param source + * .bzip2 file + * @param destination + * destination folder where the contents of the file are going to be decompressed + * @throws FileNotFoundException if the .bzip2 file is not found or does not exist + * @throws IOException if the source file already exists or there is any error with the decompression + */ + public static void decompress(File source, File destination) throws FileNotFoundException, IOException { + try ( + BZip2CompressorInputStream input = new BZip2CompressorInputStream(new BufferedInputStream(new FileInputStream(source))); + FileOutputStream output = new FileOutputStream(destination); + ) { + IOUtils.copy(input, output); + } + } + + /** Untar an input file into an output file. + + * The output file is created in the output folder, having the same name + * as the input file, minus the '.tar' extension. + * + * @param inputFile the input .tar file + * @param outputDir the output directory file. + * @throws IOException + * @throws FileNotFoundException + * + * @throws ArchiveException + */ + private static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException { + + final InputStream is = new FileInputStream(inputFile); + final TarArchiveInputStream debInputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is); + TarArchiveEntry entry = null; + while ((entry = (TarArchiveEntry)debInputStream.getNextEntry()) != null) { + final File outputFile = new File(outputDir, entry.getName()); + if (entry.isDirectory()) { + if (!outputFile.exists()) { + if (!outputFile.mkdirs()) { + throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); + } + } + } else { + final OutputStream outputFileStream = new FileOutputStream(outputFile); + IOUtils.copy(debInputStream, outputFileStream); + outputFileStream.close(); + } + } + debInputStream.close(); + + } + + public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException { + String tarPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar"; + String mambaPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\mamba"; + decompress(new File("C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar.bz2"), + new File(tarPath)); + unTar(new File(tarPath), new File(mambaPath)); + } + + // Size of the block in a standard tar file. + private static final int BLOCK_SIZE = 512; + + public static void tarDecompress(String tarFilePath, String outputDirPath) { + + File tarFile = new File(tarFilePath); + File outputDir = new File(outputDirPath); + + // Make sure the output directory exists + if (!outputDir.isDirectory()) + outputDir.mkdirs(); + + try (FileInputStream fis = new FileInputStream(tarFile); + BufferedInputStream bis = new BufferedInputStream(fis)) { + + boolean endOfArchive = false; + byte[] block = new byte[BLOCK_SIZE]; + while (!endOfArchive) { + // Read a block from the tar archive. + int bytesRead = bis.read(block); + if (bytesRead < BLOCK_SIZE) { + throw new IOException("Incomplete block read."); + } + + // Check for the end of the archive. An empty block signals end. + endOfArchive = isEndOfArchive(block); + if (endOfArchive) { + break; + } + + // Read the header from the block. + TarHeader header = new TarHeader(block); + + // If the file size is nonzero, create an output file. + if (header.fileSize > 0) { + File outputFile = new File(outputDir, header.fileName); + if (header.fileType == TarHeader.FileType.DIRECTORY) { + outputFile.mkdirs(); + } else { + try (FileOutputStream fos = new FileOutputStream(outputFile); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + long fileSizeRemaining = header.fileSize; + while (fileSizeRemaining > 0) { + int toRead = (int) Math.min(fileSizeRemaining, BLOCK_SIZE); + bytesRead = bis.read(block, 0, toRead); + if (bytesRead != toRead) { + throw new IOException("Unexpected end of file"); + } + bos.write(block, 0, bytesRead); + fileSizeRemaining -= bytesRead; + } + } + } + } + + // Skip to the next file entry in the tar archive by advancing to the next block boundary. + long fileEntrySize = (header.fileSize + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE; + long bytesToSkip = fileEntrySize - header.fileSize; + long skipped = bis.skip(bytesToSkip); + if (skipped != bytesToSkip) { + throw new IOException("Failed to skip bytes for the next entry"); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static boolean isEndOfArchive(byte[] block) { + // An empty block signals the end of the archive in a tar file. + for (int i = 0; i < BLOCK_SIZE; i++) { + if (block[i] != 0) { + return false; + } + } + return true; + } + + private static class TarHeader { + String fileName; + int fileMode; + int ownerId; + int groupId; + long fileSize; + long modificationTime; + int checksum; + FileType fileType; + String linkName; + String magic; // UStar indicator + String version; + String ownerUserName; + String ownerGroupName; + String devMajor; + String devMinor; + String prefix; // Used if the file name is longer than 100 characters + + enum FileType { + FILE, DIRECTORY, SYMLINK, CHARACTER_DEVICE, BLOCK_DEVICE, FIFO, CONTIGUOUS_FILE, GLOBAL_EXTENDED_HEADER, EXTENDED_HEADER, OTHER + } + + TarHeader(byte[] headerBlock) { + fileName = extractString(headerBlock, 0, 100); + fileMode = (int) extractOctal(headerBlock, 100, 8); + ownerId = (int) extractOctal(headerBlock, 108, 8); + groupId = (int) extractOctal(headerBlock, 116, 8); + fileSize = extractOctal(headerBlock, 124, 12); + modificationTime = extractOctal(headerBlock, 136, 12); + checksum = (int) extractOctal(headerBlock, 148, 8); + fileType = determineFileType(headerBlock[156]); + linkName = extractString(headerBlock, 157, 100); + magic = extractString(headerBlock, 257, 6); + version = extractString(headerBlock, 263, 2); + ownerUserName = extractString(headerBlock, 265, 32); + ownerGroupName = extractString(headerBlock, 297, 32); + devMajor = extractString(headerBlock, 329, 8); + devMinor = extractString(headerBlock, 337, 8); + prefix = extractString(headerBlock, 345, 155); + // Note: The prefix is used in conjunction with the filename to allow for longer file names. + } + + private long extractOctal(byte[] buffer, int offset, int length) { + String octalString = extractString(buffer, offset, length); + return Long.parseLong(octalString, 8); + } + + private String extractString(byte[] buffer, int offset, int length) { + StringBuilder stringBuilder = new StringBuilder(length); + for (int i = offset; i < offset + length; i++) { + if (buffer[i] == 0) break; // Stop at the first null character. + stringBuilder.append((char) buffer[i]); + } + return stringBuilder.toString(); + } + + private FileType determineFileType(byte typeFlag) { + switch (typeFlag) { + case '0': + case '\0': + return FileType.FILE; + case '2': + return FileType.SYMLINK; + case '3': + return FileType.CHARACTER_DEVICE; + case '4': + return FileType.BLOCK_DEVICE; + case '5': + return FileType.DIRECTORY; + case '6': + return FileType.FIFO; + case '7': + return FileType.CONTIGUOUS_FILE; + case 'g': + return FileType.GLOBAL_EXTENDED_HEADER; + case 'x': + return FileType.EXTENDED_HEADER; + default: + return FileType.OTHER; + } + } + } + +} From 782f16728a61c3937176b0f276b4e5f7b5d5975e Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 01:22:36 +0100 Subject: [PATCH 005/120] finalize the methods to unbzip2 and untar --- .../java/org/apposed/appose/Bzip2Utils.java | 209 +++--------------- 1 file changed, 26 insertions(+), 183 deletions(-) diff --git a/src/main/java/org/apposed/appose/Bzip2Utils.java b/src/main/java/org/apposed/appose/Bzip2Utils.java index 6fbc29b..669a77c 100644 --- a/src/main/java/org/apposed/appose/Bzip2Utils.java +++ b/src/main/java/org/apposed/appose/Bzip2Utils.java @@ -69,7 +69,7 @@ private Bzip2Utils() { * @throws FileNotFoundException if the .bzip2 file is not found or does not exist * @throws IOException if the source file already exists or there is any error with the decompression */ - public static void decompress(File source, File destination) throws FileNotFoundException, IOException { + public static void unBZip2(File source, File destination) throws FileNotFoundException, IOException { try ( BZip2CompressorInputStream input = new BZip2CompressorInputStream(new BufferedInputStream(new FileInputStream(source))); FileOutputStream output = new FileOutputStream(destination); @@ -87,199 +87,42 @@ public static void decompress(File source, File destination) throws FileNotFound * @param outputDir the output directory file. * @throws IOException * @throws FileNotFoundException - * * @throws ArchiveException */ private static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException { - final InputStream is = new FileInputStream(inputFile); - final TarArchiveInputStream debInputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is); - TarArchiveEntry entry = null; - while ((entry = (TarArchiveEntry)debInputStream.getNextEntry()) != null) { - final File outputFile = new File(outputDir, entry.getName()); - if (entry.isDirectory()) { - if (!outputFile.exists()) { - if (!outputFile.mkdirs()) { - throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); - } - } - } else { - final OutputStream outputFileStream = new FileOutputStream(outputFile); - IOUtils.copy(debInputStream, outputFileStream); - outputFileStream.close(); - } - } - debInputStream.close(); + try ( + InputStream is = new FileInputStream(inputFile); + TarArchiveInputStream debInputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is); + ) { + TarArchiveEntry entry = null; + while ((entry = (TarArchiveEntry)debInputStream.getNextEntry()) != null) { + final File outputFile = new File(outputDir, entry.getName()); + if (entry.isDirectory()) { + if (!outputFile.exists()) { + if (!outputFile.mkdirs()) { + throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); + } + } + } else { + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) + throw new IOException("Failed to create directory " + outputFile.getParentFile().getAbsolutePath()); + } + try (OutputStream outputFileStream = new FileOutputStream(outputFile)) { + IOUtils.copy(debInputStream, outputFileStream); + } + } + } + } } public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException { String tarPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar"; String mambaPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\mamba"; - decompress(new File("C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar.bz2"), + unBZip2(new File("C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar.bz2"), new File(tarPath)); unTar(new File(tarPath), new File(mambaPath)); } - - // Size of the block in a standard tar file. - private static final int BLOCK_SIZE = 512; - - public static void tarDecompress(String tarFilePath, String outputDirPath) { - - File tarFile = new File(tarFilePath); - File outputDir = new File(outputDirPath); - - // Make sure the output directory exists - if (!outputDir.isDirectory()) - outputDir.mkdirs(); - - try (FileInputStream fis = new FileInputStream(tarFile); - BufferedInputStream bis = new BufferedInputStream(fis)) { - - boolean endOfArchive = false; - byte[] block = new byte[BLOCK_SIZE]; - while (!endOfArchive) { - // Read a block from the tar archive. - int bytesRead = bis.read(block); - if (bytesRead < BLOCK_SIZE) { - throw new IOException("Incomplete block read."); - } - - // Check for the end of the archive. An empty block signals end. - endOfArchive = isEndOfArchive(block); - if (endOfArchive) { - break; - } - - // Read the header from the block. - TarHeader header = new TarHeader(block); - - // If the file size is nonzero, create an output file. - if (header.fileSize > 0) { - File outputFile = new File(outputDir, header.fileName); - if (header.fileType == TarHeader.FileType.DIRECTORY) { - outputFile.mkdirs(); - } else { - try (FileOutputStream fos = new FileOutputStream(outputFile); - BufferedOutputStream bos = new BufferedOutputStream(fos)) { - long fileSizeRemaining = header.fileSize; - while (fileSizeRemaining > 0) { - int toRead = (int) Math.min(fileSizeRemaining, BLOCK_SIZE); - bytesRead = bis.read(block, 0, toRead); - if (bytesRead != toRead) { - throw new IOException("Unexpected end of file"); - } - bos.write(block, 0, bytesRead); - fileSizeRemaining -= bytesRead; - } - } - } - } - - // Skip to the next file entry in the tar archive by advancing to the next block boundary. - long fileEntrySize = (header.fileSize + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE; - long bytesToSkip = fileEntrySize - header.fileSize; - long skipped = bis.skip(bytesToSkip); - if (skipped != bytesToSkip) { - throw new IOException("Failed to skip bytes for the next entry"); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static boolean isEndOfArchive(byte[] block) { - // An empty block signals the end of the archive in a tar file. - for (int i = 0; i < BLOCK_SIZE; i++) { - if (block[i] != 0) { - return false; - } - } - return true; - } - - private static class TarHeader { - String fileName; - int fileMode; - int ownerId; - int groupId; - long fileSize; - long modificationTime; - int checksum; - FileType fileType; - String linkName; - String magic; // UStar indicator - String version; - String ownerUserName; - String ownerGroupName; - String devMajor; - String devMinor; - String prefix; // Used if the file name is longer than 100 characters - - enum FileType { - FILE, DIRECTORY, SYMLINK, CHARACTER_DEVICE, BLOCK_DEVICE, FIFO, CONTIGUOUS_FILE, GLOBAL_EXTENDED_HEADER, EXTENDED_HEADER, OTHER - } - - TarHeader(byte[] headerBlock) { - fileName = extractString(headerBlock, 0, 100); - fileMode = (int) extractOctal(headerBlock, 100, 8); - ownerId = (int) extractOctal(headerBlock, 108, 8); - groupId = (int) extractOctal(headerBlock, 116, 8); - fileSize = extractOctal(headerBlock, 124, 12); - modificationTime = extractOctal(headerBlock, 136, 12); - checksum = (int) extractOctal(headerBlock, 148, 8); - fileType = determineFileType(headerBlock[156]); - linkName = extractString(headerBlock, 157, 100); - magic = extractString(headerBlock, 257, 6); - version = extractString(headerBlock, 263, 2); - ownerUserName = extractString(headerBlock, 265, 32); - ownerGroupName = extractString(headerBlock, 297, 32); - devMajor = extractString(headerBlock, 329, 8); - devMinor = extractString(headerBlock, 337, 8); - prefix = extractString(headerBlock, 345, 155); - // Note: The prefix is used in conjunction with the filename to allow for longer file names. - } - - private long extractOctal(byte[] buffer, int offset, int length) { - String octalString = extractString(buffer, offset, length); - return Long.parseLong(octalString, 8); - } - - private String extractString(byte[] buffer, int offset, int length) { - StringBuilder stringBuilder = new StringBuilder(length); - for (int i = offset; i < offset + length; i++) { - if (buffer[i] == 0) break; // Stop at the first null character. - stringBuilder.append((char) buffer[i]); - } - return stringBuilder.toString(); - } - - private FileType determineFileType(byte typeFlag) { - switch (typeFlag) { - case '0': - case '\0': - return FileType.FILE; - case '2': - return FileType.SYMLINK; - case '3': - return FileType.CHARACTER_DEVICE; - case '4': - return FileType.BLOCK_DEVICE; - case '5': - return FileType.DIRECTORY; - case '6': - return FileType.FIFO; - case '7': - return FileType.CONTIGUOUS_FILE; - case 'g': - return FileType.GLOBAL_EXTENDED_HEADER; - case 'x': - return FileType.EXTENDED_HEADER; - default: - return FileType.OTHER; - } - } - } - } From b5b947d1c48cbd55b29ba5ed9d862dace1a8e24f Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 01:34:52 +0100 Subject: [PATCH 006/120] imporve mamba installation --- src/main/java/org/apposed/appose/Builder.java | 4 ++-- src/main/java/org/apposed/appose/Conda.java | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index c6e6e03..87e48d2 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -34,6 +34,7 @@ import java.nio.file.Paths; public class Builder { + public Environment build() { String base = baseDir.getPath(); @@ -46,8 +47,7 @@ public Environment build() { // - Populate ${baseDirectory}/jars with Maven artifacts? try { - String minicondaBase = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "miniconda").toString(); - Conda conda = new Conda(minicondaBase); + Conda conda = new Conda(Conda.basePath); String envName = "appose"; if (conda.getEnvironmentNames().contains( envName )) { // TODO: Should we update it? For now, we just use it. diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 62d8d73..16bbd12 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -66,6 +66,8 @@ public class Conda { private final static int TIMEOUT_MILLIS = 10 * 1000; + final public static String basePath = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); + private final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; @@ -132,19 +134,21 @@ public Conda( final String rootdir ) throws IOException, InterruptedException if ( Files.notExists( Paths.get( rootdir ) ) ) { - final File tempFile = File.createTempFile( "miniconda", SystemUtils.IS_OS_WINDOWS ? ".tar.bz2" : ".sh" ); + final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); tempFile.deleteOnExit(); FileUtils.copyURLToFile( new URL( MICROMAMBA_URL ), tempFile, TIMEOUT_MILLIS, TIMEOUT_MILLIS ); + + final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); + tempTarFile.deleteOnExit(); + Bzip2Utils.unBZip2(tempFile, tempTarFile); + File mambaBaseDir = new File(basePath); + if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) + throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); - String command = "tar xf " + tempFile.getAbsolutePath(); - // Setting up the ProcessBuilder to use PowerShell - ProcessBuilder processBuilder = new ProcessBuilder("powershell.exe", "-Command", command); - if ( processBuilder.inheritIO().start().waitFor() != 0 ) - throw new RuntimeException(); final List< String > cmd = getBaseCommand(); if ( SystemUtils.IS_OS_WINDOWS ) @@ -176,7 +180,8 @@ private List< String > getBaseCommand() { final List< String > cmd = new ArrayList<>(); if ( SystemUtils.IS_OS_WINDOWS ) - cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); + cmd.addAll( Arrays.asList( basePath + File.separator + "Library" + + File.separator + "bin" + File.separator + "micromamba.exe", "/c" ) ); return cmd; } From 73afaf75084c334497a16ed3c10f9e6a0a84c22b Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 02:02:15 +0100 Subject: [PATCH 007/120] adapt to micromamba --- src/main/java/org/apposed/appose/Builder.java | 2 +- src/main/java/org/apposed/appose/Conda.java | 31 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 87e48d2..2eaaecf 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -47,7 +47,7 @@ public Environment build() { // - Populate ${baseDirectory}/jars with Maven artifacts? try { - Conda conda = new Conda(Conda.basePath); + Conda conda = new Conda(Conda.BASE_PATH); String envName = "appose"; if (conda.getEnvironmentNames().contains( envName )) { // TODO: Should we update it? For now, we just use it. diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 16bbd12..a0c3419 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -56,7 +56,9 @@ public class Conda { final String pythonCommand = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; - final String condaCommand = SystemUtils.IS_OS_WINDOWS ? "condabin\\conda.bat" : "condabin/conda"; + final String condaCommand = SystemUtils.IS_OS_WINDOWS ? + BASE_PATH + File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" + : BASE_PATH + File.separator + "bin" + File.separator + "micromamba"; private String envName = DEFAULT_ENVIRONMENT_NAME; @@ -66,7 +68,9 @@ public class Conda { private final static int TIMEOUT_MILLIS = 10 * 1000; - final public static String basePath = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); + final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); + + final public static String ENVS_PATH = Paths.get(BASE_PATH, "envs").toString(); private final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; @@ -145,7 +149,7 @@ public Conda( final String rootdir ) throws IOException, InterruptedException final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); tempTarFile.deleteOnExit(); Bzip2Utils.unBZip2(tempFile, tempTarFile); - File mambaBaseDir = new File(basePath); + File mambaBaseDir = new File(BASE_PATH); if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); @@ -180,8 +184,7 @@ private List< String > getBaseCommand() { final List< String > cmd = new ArrayList<>(); if ( SystemUtils.IS_OS_WINDOWS ) - cmd.addAll( Arrays.asList( basePath + File.separator + "Library" - + File.separator + "bin" + File.separator + "micromamba.exe", "/c" ) ); + cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); return cmd; } @@ -271,8 +274,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runConda( "env", "create", "--prefix", - rootdir + File.separator + "envs", "--force", - "-n", envName, "--file", envYaml, "-y" ); + ENVS_PATH + File.separator + envName, "--force", "--file", envYaml, "-y" ); } /** @@ -312,7 +314,7 @@ public void create( final String envName, final boolean isForceCreation ) throws { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runConda( "create", "-y", "-n", envName ); + runConda( "create", "-y", "-p", ENVS_PATH + File.separator + envName ); } /** @@ -360,7 +362,7 @@ public void create( final String envName, final boolean isForceCreation, final S { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-n", envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", ENVS_PATH + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); runConda( cmd.stream().toArray( String[]::new ) ); } @@ -554,7 +556,7 @@ public void runPythonIn( final String envName, final String... args ) throws IOE envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); envs.put( "Path", Paths.get( envDir, "Library", "Bin" ).toString() + ";" + envs.get( "Path" ) ); } - builder.environment().putAll( getEnvironmentVariables( envName ) ); + // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); if ( builder.command( cmd ).start().waitFor() != 0 ) throw new RuntimeException(); } @@ -573,7 +575,7 @@ public void runPythonIn( final String envName, final String... args ) throws IOE public String getVersion() throws IOException, InterruptedException { final List< String > cmd = getBaseCommand(); - cmd.addAll( Arrays.asList( condaCommand, "-V" ) ); + cmd.addAll( Arrays.asList( condaCommand, "--version" ) ); final Process process = getBuilder( false ).command( cmd ).start(); if ( process.waitFor() != 0 ) throw new RuntimeException(); @@ -613,10 +615,12 @@ public void runConda( final String... args ) throws RuntimeException, IOExceptio * is waiting, then the wait is ended and an InterruptedException is * thrown. */ + /* TODO find equivalent in mamba public Map< String, String > getEnvironmentVariables() throws IOException, InterruptedException { return getEnvironmentVariables( envName ); } + */ /** * Returns environment variables associated with the specified environment as @@ -632,6 +636,8 @@ public Map< String, String > getEnvironmentVariables() throws IOException, Inter * is waiting, then the wait is ended and an InterruptedException is * thrown. */ + /** + * TODO find equivalent in mamba public Map< String, String > getEnvironmentVariables( final String envName ) throws IOException, InterruptedException { final List< String > cmd = getBaseCommand(); @@ -652,6 +658,7 @@ public Map< String, String > getEnvironmentVariables( final String envName ) thr } return map; } + */ /** * Returns a list of the Conda environment names as {@code List< String >}. @@ -667,7 +674,7 @@ public Map< String, String > getEnvironmentVariables( final String envName ) thr public List< String > getEnvironmentNames() throws IOException { final List< String > envs = new ArrayList<>( Arrays.asList( DEFAULT_ENVIRONMENT_NAME ) ); - envs.addAll( Files.list( Paths.get( rootdir, "envs" ) ) + envs.addAll( Files.list( Paths.get( ENVS_PATH ) ) .map( p -> p.getFileName().toString() ) .filter( p -> !p.startsWith( "." ) ) .collect( Collectors.toList() ) ); From d33b7858681e3595477393cf85380b09f9be7e02 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 02:02:57 +0100 Subject: [PATCH 008/120] remove not used anymore dep --- pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cea0ebe..5d89697 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.apposed appose - 0.1.1-SNAPSHOT + 0.1.1 Appose Appose: multi-language interprocess cooperation with shared memory. @@ -132,5 +132,10 @@ junit-jupiter-engine test + + org.apache.commons + commons-compress + 1.24.0 + From bd36222a9919e21bed41cc20aa6c526c651095ae Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 02:08:25 +0100 Subject: [PATCH 009/120] remove forgotten import --- src/main/java/org/apposed/appose/Bzip2Utils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Bzip2Utils.java b/src/main/java/org/apposed/appose/Bzip2Utils.java index 669a77c..1efd7e3 100644 --- a/src/main/java/org/apposed/appose/Bzip2Utils.java +++ b/src/main/java/org/apposed/appose/Bzip2Utils.java @@ -45,7 +45,6 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.utils.IOUtils; -import org.itadaki.bzip2.BZip2InputStream; /** * Utility methods unzip bzip2 files From 450d73ffb48b7910641c47660d30f644d2c7d39e Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 03:29:08 +0100 Subject: [PATCH 010/120] install correctly mamba --- src/main/java/org/apposed/appose/Conda.java | 36 +++++---- ...ip2Utils.java => MambaInstallerUtils.java} | 76 +++++++++++++++++-- 2 files changed, 86 insertions(+), 26 deletions(-) rename src/main/java/org/apposed/appose/{Bzip2Utils.java => MambaInstallerUtils.java} (66%) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index a0c3419..b2436dc 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -26,6 +26,7 @@ ******************************************************************************/ package org.apposed.appose; +import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.apposed.appose.CondaException.EnvironmentExistsException; @@ -34,9 +35,13 @@ import java.io.BufferedReader; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -72,7 +77,7 @@ public class Conda { final public static String ENVS_PATH = Paths.get(BASE_PATH, "envs").toString(); - private final static String MICROMAMBA_URL = + public final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; private static String microMambaPlatform() { @@ -132,35 +137,28 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws ArchiveException + * @throws URISyntaxException */ - public Conda( final String rootdir ) throws IOException, InterruptedException + public Conda( final String rootdir ) throws IOException, InterruptedException, ArchiveException, URISyntaxException { - if ( Files.notExists( Paths.get( rootdir ) ) ) + if ( Files.notExists( Paths.get( condaCommand ) ) ) { final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); tempFile.deleteOnExit(); - FileUtils.copyURLToFile( - new URL( MICROMAMBA_URL ), - tempFile, - TIMEOUT_MILLIS, - TIMEOUT_MILLIS ); - + URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); tempTarFile.deleteOnExit(); - Bzip2Utils.unBZip2(tempFile, tempTarFile); + MambaInstallerUtils.unBZip2(tempFile, tempTarFile); File mambaBaseDir = new File(BASE_PATH); if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); - - final List< String > cmd = getBaseCommand(); - - if ( SystemUtils.IS_OS_WINDOWS ) - cmd.addAll( Arrays.asList( "start", "/wait", "\"\"", tempFile.getAbsolutePath(), "/InstallationType=JustMe", "/AddToPath=0", "/RegisterPython=0", "/S", "/D=" + rootdir ) ); - else - cmd.addAll( Arrays.asList( "bash", tempFile.getAbsolutePath(), "-b", "-p", rootdir ) ); - if ( new ProcessBuilder().inheritIO().command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException(); + MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); } this.rootdir = rootdir; diff --git a/src/main/java/org/apposed/appose/Bzip2Utils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java similarity index 66% rename from src/main/java/org/apposed/appose/Bzip2Utils.java rename to src/main/java/org/apposed/appose/MambaInstallerUtils.java index 1efd7e3..b64749c 100644 --- a/src/main/java/org/apposed/appose/Bzip2Utils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -30,7 +30,6 @@ package org.apposed.appose; import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -38,6 +37,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveStreamFactory; @@ -49,11 +55,9 @@ /** * Utility methods unzip bzip2 files */ -public final class Bzip2Utils { +public final class MambaInstallerUtils { - final private static int BUFFER_SIZE = 1024 * 20; - - private Bzip2Utils() { + private MambaInstallerUtils() { // Prevent instantiation of utility class. } @@ -88,7 +92,7 @@ public static void unBZip2(File source, File destination) throws FileNotFoundExc * @throws FileNotFoundException * @throws ArchiveException */ - private static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException { + public static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException { try ( InputStream is = new FileInputStream(inputFile); @@ -117,11 +121,69 @@ private static void unTar(final File inputFile, final File outputDir) throws Fil } - public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException { + public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException { + String url = Conda.MICROMAMBA_URL; + final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); + tempFile.deleteOnExit(); + URL website = MambaInstallerUtils.redirectedURL(new URL(url)); + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + long transferred = fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + System.out.print(tempFile.length()); + } String tarPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar"; String mambaPath = "C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\mamba"; unBZip2(new File("C:\\Users\\angel\\OneDrive\\Documentos\\pasteur\\git\\micromamba-1.5.1-1.tar.bz2"), new File(tarPath)); unTar(new File(tarPath), new File(mambaPath)); } + + /** + * This method shuold be used when we get the following response codes from + * a {@link HttpURLConnection}: + * - {@link HttpURLConnection#HTTP_MOVED_TEMP} + * - {@link HttpURLConnection#HTTP_MOVED_PERM} + * - {@link HttpURLConnection#HTTP_SEE_OTHER} + * + * If that is not the response code or the connection does not work, the url + * returned will be the same as the provided. + * If the method is used corretly, it will return the URL to which the original URL + * has been redirected + * @param url + * original url. Connecting to that url must give a 301, 302 or 303 response code + * @param conn + * connection to the url + * @return the redirected url + * @throws MalformedURLException + * @throws URISyntaxException + */ + public static URL redirectedURL(URL url) throws MalformedURLException, URISyntaxException { + int statusCode; + HttpURLConnection conn; + try { + conn = (HttpURLConnection) url.openConnection(); + statusCode = conn.getResponseCode(); + } catch (IOException ex) { + return url; + } + if (statusCode < 300 || statusCode > 308) + return url; + String newURL = conn.getHeaderField("Location"); + try { + return redirectedURL(new URL(newURL)); + } catch (MalformedURLException ex) { + } + try { + if (newURL.startsWith("//")) + return redirectedURL(new URL("http:" + newURL)); + else + throw new MalformedURLException(); + } catch (MalformedURLException ex) { + } + URI uri = url.toURI(); + String scheme = uri.getScheme(); + String host = uri.getHost(); + String mainDomain = scheme + "://" + host; + return redirectedURL(new URL(mainDomain + newURL)); + } } From 619d9fa8eaf51c64ff234fbbfebee31009ac1ad5 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 03:56:49 +0100 Subject: [PATCH 011/120] correct small errors --- src/main/java/org/apposed/appose/Builder.java | 7 +++++++ src/main/java/org/apposed/appose/Conda.java | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 2eaaecf..343023f 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -31,8 +31,11 @@ import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Paths; +import org.apache.commons.compress.archivers.ArchiveException; + public class Builder { @@ -59,6 +62,10 @@ public Environment build() { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); + } catch (ArchiveException e) { + throw new RuntimeException(e); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } return new Environment() { diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index b2436dc..5d9d287 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -159,15 +159,15 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); + if (!(new File(ENVS_PATH)).isDirectory() && !new File(ENVS_PATH).mkdirs()) + throw new IOException("Failed to create Micromamba default envs directory " + ENVS_PATH); + } this.rootdir = rootdir; // The following command will throw an exception if Conda does not work as // expected. - final List< String > cmd = getBaseCommand(); - cmd.addAll( Arrays.asList( condaCommand, "-V" ) ); - if ( getBuilder( false ).command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException(); + getVersion(); } /** @@ -244,7 +244,7 @@ public void updateIn( final String envName, final String... args ) throws IOExce */ public void createWithYaml( final String envName, final String envYaml ) throws IOException, InterruptedException { - createWithYaml(envName, envYaml); + createWithYaml(envName, envYaml, false); } /** From f5aab6038c64044ef0f1ab861e65e95dfda06156 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Fri, 3 Nov 2023 04:20:51 +0100 Subject: [PATCH 012/120] adopt mamba syntax --- src/main/java/org/apposed/appose/Conda.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 5d9d287..b8c18e9 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -272,7 +272,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runConda( "env", "create", "--prefix", - ENVS_PATH + File.separator + envName, "--force", "--file", envYaml, "-y" ); + ENVS_PATH + File.separator + envName, "-f", envYaml, "-y" ); } /** From 2a8b81df3e1a8dcf9041809cfeb819b58258e336 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 6 Nov 2023 17:19:26 +0100 Subject: [PATCH 013/120] fix paths --- src/main/java/org/apposed/appose/Conda.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index b8c18e9..65c905a 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -27,12 +27,9 @@ package org.apposed.appose; import org.apache.commons.compress.archivers.ArchiveException; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.apposed.appose.CondaException.EnvironmentExistsException; -import groovy.json.JsonSlurper; - import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -46,7 +43,6 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -61,9 +57,7 @@ public class Conda { final String pythonCommand = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; - final String condaCommand = SystemUtils.IS_OS_WINDOWS ? - BASE_PATH + File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" - : BASE_PATH + File.separator + "bin" + File.separator + "micromamba"; + final String condaCommand; private String envName = DEFAULT_ENVIRONMENT_NAME; @@ -72,6 +66,10 @@ public class Conda { public final static String DEFAULT_ENVIRONMENT_NAME = "base"; private final static int TIMEOUT_MILLIS = 10 * 1000; + + private final static String CONDA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? + File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" + : File.separator + "bin" + File.separator + "micromamba"; final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); @@ -142,6 +140,11 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) */ public Conda( final String rootdir ) throws IOException, InterruptedException, ArchiveException, URISyntaxException { + if (rootdir == null) + this.rootdir = BASE_PATH; + else + this.rootdir = rootdir; + this.condaCommand = this.rootdir + CONDA_RELATIVE_PATH; if ( Files.notExists( Paths.get( condaCommand ) ) ) { @@ -163,7 +166,6 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A throw new IOException("Failed to create Micromamba default envs directory " + ENVS_PATH); } - this.rootdir = rootdir; // The following command will throw an exception if Conda does not work as // expected. From 0bbb2ad40db9a4ea3e8abd2c587766efaed6486a Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 6 Nov 2023 17:56:29 +0100 Subject: [PATCH 014/120] add consumer to conda run to keep track of conda env installation --- src/main/java/org/apposed/appose/Conda.java | 100 +++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 65c905a..ba330a1 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -41,10 +41,13 @@ import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Paths; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.stream.Collectors; /** @@ -249,6 +252,27 @@ public void createWithYaml( final String envName, final String envYaml ) throws createWithYaml(envName, envYaml, false); } + /** + * Run {@code conda create} to create a conda environment defined by the input environment yaml file. + * + * @param envName + * The environment name to be created. + * @param envYaml + * The environment yaml file containing the information required to build it + * @param consumer + * String consumer that keeps track of the environment creation + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void createWithYaml( final String envName, final String envYaml, Consumer consumer ) throws IOException, InterruptedException + { + createWithYaml(envName, envYaml, false, consumer); + } + /** * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * @@ -277,6 +301,36 @@ public void createWithYaml( final String envName, final String envYaml, final bo ENVS_PATH + File.separator + envName, "-f", envYaml, "-y" ); } + /** + * Run {@code conda create} to create a conda environment defined by the input environment yaml file. + * + * @param envName + * The environment name to be created. + * @param envYaml + * The environment yaml file containing the information required to build it + * @param envName + * The environment name to be created. + * @param isForceCreation + * Force creation of the environment if {@code true}. If this value + * is {@code false} and an environment with the specified name + * already exists, throw an {@link EnvironmentExistsException}. + * @param consumer + * String consumer that keeps track of the environment creation + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation, Consumer consumer) throws IOException, InterruptedException + { + if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) + throw new EnvironmentExistsException(); + runConda(consumer, "env", "create", "--prefix", + ENVS_PATH + File.separator + envName, "-f", envYaml, "-y" ); + } + /** * Run {@code conda create} to create an empty conda environment. * @@ -582,6 +636,50 @@ public String getVersion() throws IOException, InterruptedException return new BufferedReader( new InputStreamReader( process.getInputStream() ) ).readLine(); } + /** + * Run a Conda command with one or more arguments. + * + * @param consumer + * String consumer that receives the Strings that the process prints to the console + * @param args + * One or more arguments for the Conda command. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void runConda(Consumer consumer, final String... args ) throws RuntimeException, IOException, InterruptedException + { + Calendar cal = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); + + final List< String > cmd = getBaseCommand(); + cmd.add( condaCommand ); + cmd.addAll( Arrays.asList( args ) ); + + ProcessBuilder builder = getBuilder(true).command(cmd); + Process process = builder.start(); + try ( + BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + ) { + String line = outReader.readLine(); + String errLine = errReader.readLine(); + while (line != null || errLine != null) { + if (line != null) + consumer.accept(sdf.format(cal.getTime()) + " -- PROGRESS -> " + line); + if (errLine != null) + consumer.accept(sdf.format(cal.getTime()) + " -- ERROR -> " + errLine); + line = outReader.readLine(); + errLine = errReader.readLine(); + } + if (process.waitFor() != 0) + throw new RuntimeException("Error executing the following command: " + builder.command()); + } + } + /** * Run a Conda command with one or more arguments. * @@ -594,7 +692,7 @@ public String getVersion() throws IOException, InterruptedException * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void runConda( final String... args ) throws RuntimeException, IOException, InterruptedException + public void runConda(final String... args ) throws RuntimeException, IOException, InterruptedException { final List< String > cmd = getBaseCommand(); cmd.add( condaCommand ); From 4d2b25fcdc6f7a7fd89cca96f9bcd7711f7dc25e Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 6 Nov 2023 19:38:21 +0100 Subject: [PATCH 015/120] make verbose --- src/main/java/org/apposed/appose/Conda.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index ba330a1..82b334c 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -328,7 +328,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runConda(consumer, "env", "create", "--prefix", - ENVS_PATH + File.separator + envName, "-f", envYaml, "-y" ); + ENVS_PATH + File.separator + envName, "-f", envYaml, "-y", "-vv" ); } /** From 92c1cbeccff13a30afa655e8f70f78ccafa0069f Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 7 Nov 2023 03:29:14 +0100 Subject: [PATCH 016/120] send information when creating an env or runnning conda env --- src/main/java/org/apposed/appose/Conda.java | 61 ++++++++++++++------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 82b334c..479b399 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -652,32 +652,55 @@ public String getVersion() throws IOException, InterruptedException */ public void runConda(Consumer consumer, final String... args ) throws RuntimeException, IOException, InterruptedException { - Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); final List< String > cmd = getBaseCommand(); cmd.add( condaCommand ); cmd.addAll( Arrays.asList( args ) ); - ProcessBuilder builder = getBuilder(true).command(cmd); + ProcessBuilder builder = getBuilder(false).command(cmd); Process process = builder.start(); - try ( - BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - ) { - String line = outReader.readLine(); - String errLine = errReader.readLine(); - while (line != null || errLine != null) { - if (line != null) - consumer.accept(sdf.format(cal.getTime()) + " -- PROGRESS -> " + line); - if (errLine != null) - consumer.accept(sdf.format(cal.getTime()) + " -- ERROR -> " + errLine); - line = outReader.readLine(); - errLine = errReader.readLine(); - } - if (process.waitFor() != 0) - throw new RuntimeException("Error executing the following command: " + builder.command()); - } + // Use separate threads to read each stream to avoid a deadlock. + Thread outputThread = new Thread(() -> { + long updatePeriod = 300; + try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + String chunk = ""; + long t0 = System.currentTimeMillis(); + while ((line = outReader.readLine()) != null || process.isAlive()) { + if (line == null) continue; + chunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + line + System.lineSeparator(); + if (System.currentTimeMillis() - t0 > updatePeriod) { + consumer.accept(chunk); + chunk = ""; + t0 = System.currentTimeMillis(); + } + } + consumer.accept(chunk); + } catch (IOException e) { + e.printStackTrace(); + } + }); + + Thread errorThread = new Thread(() -> { + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = errReader.readLine()) != null || process.isAlive()) { + if (line != null) System.err.println(sdf.format(Calendar.getInstance().getTime()) + ": " + line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + // Start reading threads + outputThread.start(); + errorThread.start(); + int processResult = process.waitFor(); + // Wait for all output to be read + outputThread.join(); + errorThread.join(); + if (processResult != 0) + throw new RuntimeException("Error executing the following command: " + builder.command()); } /** From 40c46b4ec1ef0d29712cd4615f8f6fb1114b6775 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 7 Nov 2023 16:34:50 +0100 Subject: [PATCH 017/120] send err stream --- src/main/java/org/apposed/appose/Conda.java | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 479b399..883d36f 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -47,6 +47,7 @@ import java.util.Calendar; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -67,8 +68,6 @@ public class Conda { private final String rootdir; public final static String DEFAULT_ENVIRONMENT_NAME = "base"; - - private final static int TIMEOUT_MILLIS = 10 * 1000; private final static String CONDA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" @@ -80,6 +79,8 @@ public class Conda { public final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; + + public final static String ERR_STREAM_UUUID = UUID.randomUUID().toString(); private static String microMambaPlatform() { String osName = System.getProperty("os.name"); @@ -661,8 +662,8 @@ public void runConda(Consumer consumer, final String... args ) throws Ru ProcessBuilder builder = getBuilder(false).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. + long updatePeriod = 300; Thread outputThread = new Thread(() -> { - long updatePeriod = 300; try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; String chunk = ""; @@ -676,7 +677,7 @@ public void runConda(Consumer consumer, final String... args ) throws Ru t0 = System.currentTimeMillis(); } } - consumer.accept(chunk); + consumer.accept(chunk + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED INSTALLATION"); } catch (IOException e) { e.printStackTrace(); } @@ -684,9 +685,17 @@ public void runConda(Consumer consumer, final String... args ) throws Ru Thread errorThread = new Thread(() -> { try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - String line; + String line; + String chunk = ""; + long t0 = System.currentTimeMillis(); while ((line = errReader.readLine()) != null || process.isAlive()) { - if (line != null) System.err.println(sdf.format(Calendar.getInstance().getTime()) + ": " + line); + if (line == null) continue; + chunk += ERR_STREAM_UUUID + line + System.lineSeparator(); + if (System.currentTimeMillis() - t0 > updatePeriod) { + consumer.accept(chunk); + chunk = ""; + t0 = System.currentTimeMillis(); + } } } catch (IOException e) { e.printStackTrace(); From 7029a9094d6e5f357803fe2b9ea555dab14301a5 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 8 Nov 2023 11:08:52 +0100 Subject: [PATCH 018/120] add permissions to executable and create default Conda builder --- src/main/java/org/apposed/appose/Conda.java | 41 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 883d36f..2263138 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -115,6 +115,37 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) return builder; } + /** + * Create a new Conda object. The root dir for the Micromamba installation + * will be /user/.local/share/appose/micromamba. + * If there is no directory found at the specified + * path, Miniconda will be automatically installed in the path. It is expected + * that the Conda installation has executable commands as shown below: + * + *

+	 * CONDA_ROOT
+	 * ├── condabin
+	 * │   ├── conda(.bat)
+	 * │   ... 
+	 * ├── envs
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
+	 * 
+ * + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + * @throws ArchiveException + * @throws URISyntaxException + */ + public Conda() throws IOException, InterruptedException, ArchiveException, URISyntaxException + { + this(BASE_PATH); + } + /** * Create a new Conda object. The root dir for Conda installation can be * specified as {@code String}. If there is no directory found at the specified @@ -171,6 +202,13 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A } + // The following command will throw an exception if Conda does not work as + // expected. + boolean executableSet = new File(condaCommand).setExecutable(true); + if (!executableSet) + throw new IOException("Cannot set file as executable due to missing permissions, " + + "please do it manually: " + condaCommand); + // The following command will throw an exception if Conda does not work as // expected. getVersion(); @@ -662,7 +700,8 @@ public void runConda(Consumer consumer, final String... args ) throws Ru ProcessBuilder builder = getBuilder(false).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. - long updatePeriod = 300; + consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); + long updatePeriod = 300; Thread outputThread = new Thread(() -> { try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; From 79be3ce1929f1840b3c7f3b5c13c265d453092b0 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 8 Nov 2023 22:34:46 +0100 Subject: [PATCH 019/120] update paths --- src/main/java/org/apposed/appose/Conda.java | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 2263138..deca484 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -67,6 +67,8 @@ public class Conda { private final String rootdir; + private final String envsdir; + public final static String DEFAULT_ENVIRONMENT_NAME = "base"; private final static String CONDA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? @@ -75,7 +77,7 @@ public class Conda { final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); - final public static String ENVS_PATH = Paths.get(BASE_PATH, "envs").toString(); + final public static String ENVS_NAME = "envs"; public final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; @@ -180,6 +182,7 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A else this.rootdir = rootdir; this.condaCommand = this.rootdir + CONDA_RELATIVE_PATH; + this.envsdir = this.rootdir + File.separator + "envs"; if ( Files.notExists( Paths.get( condaCommand ) ) ) { @@ -193,12 +196,12 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); - File mambaBaseDir = new File(BASE_PATH); + File mambaBaseDir = new File(rootdir); if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); - if (!(new File(ENVS_PATH)).isDirectory() && !new File(ENVS_PATH).mkdirs()) - throw new IOException("Failed to create Micromamba default envs directory " + ENVS_PATH); + if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) + throw new IOException("Failed to create Micromamba default envs directory " + envsdir); } @@ -213,6 +216,10 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A // expected. getVersion(); } + + public String getEnvsDir() { + return this.envsdir; + } /** * Returns {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for @@ -337,7 +344,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runConda( "env", "create", "--prefix", - ENVS_PATH + File.separator + envName, "-f", envYaml, "-y" ); + envsdir + File.separator + envName, "-f", envYaml, "-y" ); } /** @@ -367,7 +374,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runConda(consumer, "env", "create", "--prefix", - ENVS_PATH + File.separator + envName, "-f", envYaml, "-y", "-vv" ); + envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); } /** @@ -407,7 +414,7 @@ public void create( final String envName, final boolean isForceCreation ) throws { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runConda( "create", "-y", "-p", ENVS_PATH + File.separator + envName ); + runConda( "create", "-y", "-p", envsdir + File.separator + envName ); } /** @@ -455,7 +462,7 @@ public void create( final String envName, final boolean isForceCreation, final S { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", ENVS_PATH + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); runConda( cmd.stream().toArray( String[]::new ) ); } @@ -701,7 +708,7 @@ public void runConda(Consumer consumer, final String... args ) throws Ru Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); - long updatePeriod = 300; + long updatePeriod = 500; Thread outputThread = new Thread(() -> { try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; @@ -843,7 +850,7 @@ public Map< String, String > getEnvironmentVariables( final String envName ) thr public List< String > getEnvironmentNames() throws IOException { final List< String > envs = new ArrayList<>( Arrays.asList( DEFAULT_ENVIRONMENT_NAME ) ); - envs.addAll( Files.list( Paths.get( ENVS_PATH ) ) + envs.addAll( Files.list( Paths.get( envsdir ) ) .map( p -> p.getFileName().toString() ) .filter( p -> !p.startsWith( "." ) ) .collect( Collectors.toList() ) ); From f0e981b0fda4e03f172a8d7295986763f0da306c Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 22 Nov 2023 23:24:18 +0100 Subject: [PATCH 020/120] rduce waiting time --- src/main/java/org/apposed/appose/Conda.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index deca484..fe1397e 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -708,7 +708,7 @@ public void runConda(Consumer consumer, final String... args ) throws Ru Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); - long updatePeriod = 500; + long updatePeriod = 00; Thread outputThread = new Thread(() -> { try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; From 06b629be6cf08b8c0b7c06a3a58f7dfd4bf014e5 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 22 Nov 2023 23:55:10 +0100 Subject: [PATCH 021/120] reduce the update period --- src/main/java/org/apposed/appose/Conda.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index fe1397e..78876c4 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -708,7 +708,7 @@ public void runConda(Consumer consumer, final String... args ) throws Ru Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); - long updatePeriod = 00; + long updatePeriod = 200; Thread outputThread = new Thread(() -> { try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; From 3894a92eccb6eca37ff8047105ae725120b31be6 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 23 Nov 2023 15:54:37 +0100 Subject: [PATCH 022/120] improve log of env installation, more reactive --- src/main/java/org/apposed/appose/Conda.java | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 78876c4..87382e6 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -34,11 +34,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import java.nio.channels.Selector; import java.nio.file.Files; import java.nio.file.Paths; import java.text.SimpleDateFormat; @@ -710,6 +712,54 @@ public void runConda(Consumer consumer, final String... args ) throws Ru consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); long updatePeriod = 200; Thread outputThread = new Thread(() -> { + try ( + InputStream inputStream = process.getInputStream(); + InputStream errStream = process.getErrorStream(); + ){ + byte[] buffer = new byte[1024]; // Buffer size can be adjusted + StringBuilder processBuff = new StringBuilder(); + StringBuilder errBuff = new StringBuilder(); + String processChunk = ""; + String errChunk = ""; + long t0 = System.currentTimeMillis(); + while (process.isAlive() || inputStream.available() > 0) { + if (inputStream.available() > 0) { + String current = new String(buffer, 0, inputStream.read(buffer)); + processBuff.append(current); + int newLineIndex; + while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { + processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + processBuff.substring(0, newLineIndex + 1).trim(); + processBuff.delete(0, newLineIndex + 1); + } + } + if (errStream.available() > 0) { + String current = new String(buffer, 0, errStream.read(buffer)); + errBuff.append(current); + int newLineIndex; + while ((newLineIndex = errBuff.indexOf(System.lineSeparator())) != -1) { + errChunk += ERR_STREAM_UUUID + errBuff.substring(0, newLineIndex + 1).trim(); + errBuff.delete(0, newLineIndex + 1); + } + } + + // No data available, sleep for a bit to avoid busy waiting + Thread.sleep(50); + if (System.currentTimeMillis() - t0 > updatePeriod) { + consumer.accept(processChunk); + consumer.accept(errChunk); + processChunk = ""; + errChunk = ""; + t0 = System.currentTimeMillis(); + } + } + + // Process any remaining data in the buffer + if (processBuff.length() > 0) { + // Process remaining partial line here + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; String chunk = ""; From 3098f8052447baa923c613a0a025a83c009a086b Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 23 Nov 2023 17:32:58 +0100 Subject: [PATCH 023/120] use just one thread to recover process info --- src/main/java/org/apposed/appose/Conda.java | 68 +++++---------------- 1 file changed, 16 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 87382e6..b607906 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -721,89 +721,53 @@ public void runConda(Consumer consumer, final String... args ) throws Ru StringBuilder errBuff = new StringBuilder(); String processChunk = ""; String errChunk = ""; + int newLineIndex; long t0 = System.currentTimeMillis(); while (process.isAlive() || inputStream.available() > 0) { if (inputStream.available() > 0) { - String current = new String(buffer, 0, inputStream.read(buffer)); - processBuff.append(current); - int newLineIndex; + processBuff.append(new String(buffer, 0, inputStream.read(buffer))); while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + processBuff.substring(0, newLineIndex + 1).trim(); processBuff.delete(0, newLineIndex + 1); } } if (errStream.available() > 0) { - String current = new String(buffer, 0, errStream.read(buffer)); - errBuff.append(current); - int newLineIndex; + errBuff.append(new String(buffer, 0, errStream.read(buffer))); while ((newLineIndex = errBuff.indexOf(System.lineSeparator())) != -1) { errChunk += ERR_STREAM_UUUID + errBuff.substring(0, newLineIndex + 1).trim(); errBuff.delete(0, newLineIndex + 1); } } - - // No data available, sleep for a bit to avoid busy waiting + // Sleep for a bit to avoid busy waiting Thread.sleep(50); if (System.currentTimeMillis() - t0 > updatePeriod) { + consumer.accept(errChunk.equals("") ? null : errChunk); consumer.accept(processChunk); - consumer.accept(errChunk); processChunk = ""; errChunk = ""; t0 = System.currentTimeMillis(); } } - - // Process any remaining data in the buffer - if (processBuff.length() > 0) { - // Process remaining partial line here - } + if (inputStream.available() > 0) { + processBuff.append(new String(buffer, 0, inputStream.read(buffer))); + processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + processBuff.toString().trim(); + } + if (errStream.available() > 0) { + errBuff.append(new String(buffer, 0, errStream.read(buffer))); + errChunk += ERR_STREAM_UUUID + errBuff.toString().trim(); + } + consumer.accept(errChunk); + consumer.accept(processChunk + System.lineSeparator() + + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED INSTALLATION"); } catch (IOException | InterruptedException e) { e.printStackTrace(); } - try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - String chunk = ""; - long t0 = System.currentTimeMillis(); - while ((line = outReader.readLine()) != null || process.isAlive()) { - if (line == null) continue; - chunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + line + System.lineSeparator(); - if (System.currentTimeMillis() - t0 > updatePeriod) { - consumer.accept(chunk); - chunk = ""; - t0 = System.currentTimeMillis(); - } - } - consumer.accept(chunk + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED INSTALLATION"); - } catch (IOException e) { - e.printStackTrace(); - } - }); - - Thread errorThread = new Thread(() -> { - try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - String line; - String chunk = ""; - long t0 = System.currentTimeMillis(); - while ((line = errReader.readLine()) != null || process.isAlive()) { - if (line == null) continue; - chunk += ERR_STREAM_UUUID + line + System.lineSeparator(); - if (System.currentTimeMillis() - t0 > updatePeriod) { - consumer.accept(chunk); - chunk = ""; - t0 = System.currentTimeMillis(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } }); // Start reading threads outputThread.start(); - errorThread.start(); int processResult = process.waitFor(); // Wait for all output to be read outputThread.join(); - errorThread.join(); if (processResult != 0) throw new RuntimeException("Error executing the following command: " + builder.command()); } From e4814cab0e13c58ab12cc0aaad8db6f3ee740ba7 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 23 Nov 2023 18:24:41 +0100 Subject: [PATCH 024/120] correct line separation --- src/main/java/org/apposed/appose/Conda.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index b607906..932856d 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -727,14 +727,15 @@ public void runConda(Consumer consumer, final String... args ) throws Ru if (inputStream.available() > 0) { processBuff.append(new String(buffer, 0, inputStream.read(buffer))); while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { - processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + processBuff.substring(0, newLineIndex + 1).trim(); + processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + + processBuff.substring(0, newLineIndex + 1).trim() + System.lineSeparator(); processBuff.delete(0, newLineIndex + 1); } } if (errStream.available() > 0) { errBuff.append(new String(buffer, 0, errStream.read(buffer))); while ((newLineIndex = errBuff.indexOf(System.lineSeparator())) != -1) { - errChunk += ERR_STREAM_UUUID + errBuff.substring(0, newLineIndex + 1).trim(); + errChunk += ERR_STREAM_UUUID + errBuff.substring(0, newLineIndex + 1).trim() + System.lineSeparator(); errBuff.delete(0, newLineIndex + 1); } } From 004ccc4835e55fbb052f77b8afa77fa8d0bc152b Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 23 Nov 2023 19:43:31 +0100 Subject: [PATCH 025/120] add TODO --- src/main/java/org/apposed/appose/Conda.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 932856d..70adce5 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -710,7 +710,7 @@ public void runConda(Consumer consumer, final String... args ) throws Ru Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); - long updatePeriod = 200; + long updatePeriod = 300; Thread outputThread = new Thread(() -> { try ( InputStream inputStream = process.getInputStream(); @@ -740,9 +740,9 @@ public void runConda(Consumer consumer, final String... args ) throws Ru } } // Sleep for a bit to avoid busy waiting - Thread.sleep(50); + Thread.sleep(60); if (System.currentTimeMillis() - t0 > updatePeriod) { - consumer.accept(errChunk.equals("") ? null : errChunk); + // TODO decide what to do with the err stream consumer.accept(errChunk.equals("") ? null : errChunk); consumer.accept(processChunk); processChunk = ""; errChunk = ""; From cce6b6837a82d8bd3475d49dfc8a9b3419c4483a Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 23 Jan 2024 19:53:44 +0100 Subject: [PATCH 026/120] start doing method that checks whether a dependency is installed in a giv --- src/main/java/org/apposed/appose/Conda.java | 67 +++++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 70adce5..da91164 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -61,7 +61,7 @@ */ public class Conda { - final String pythonCommand = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; + final static String PYTHON_COMMAND = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; final String condaCommand; @@ -231,7 +231,7 @@ public String getEnvsDir() { * Mac/Linux. * @throws IOException */ - private List< String > getBaseCommand() + private static List< String > getBaseCommand() { final List< String > cmd = new ArrayList<>(); if ( SystemUtils.IS_OS_WINDOWS ) @@ -644,9 +644,9 @@ public void runPythonIn( final String envName, final String... args ) throws IOE { final List< String > cmd = getBaseCommand(); if ( envName.equals( DEFAULT_ENVIRONMENT_NAME ) ) - cmd.add( pythonCommand ); + cmd.add( PYTHON_COMMAND ); else - cmd.add( Paths.get( "envs", envName, pythonCommand ).toString() ); + cmd.add( Paths.get( "envs", envName, PYTHON_COMMAND ).toString() ); cmd.addAll( Arrays.asList( args ) ); final ProcessBuilder builder = getBuilder( true ); if ( SystemUtils.IS_OS_WINDOWS ) @@ -663,6 +663,47 @@ public void runPythonIn( final String envName, final String... args ) throws IOE throw new RuntimeException(); } + /** + * Run a Python command in the specified environment. This method automatically + * sets environment variables associated with the specified environment. In + * Windows, this method also sets the {@code PATH} environment variable so that + * the specified environment runs as expected. + * + * @param envName + * The environment name used to run the Python command. + * @param args + * One or more arguments for the Python command. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public static void runPythonIn( final File envFile, final String... args ) throws IOException, InterruptedException + { + if (!Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toFile().isFile()) + throw new IOException("No Python found in the environment provided. The following " + + "file does not exist: " + Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath()); + final List< String > cmd = getBaseCommand(); + cmd.add( Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath().toString() ); + cmd.addAll( Arrays.asList( args ) ); + final ProcessBuilder builder = new ProcessBuilder().directory( envFile ); + builder.inheritIO(); + if ( SystemUtils.IS_OS_WINDOWS ) + { + final Map< String, String > envs = builder.environment(); + final String envDir = envFile.getAbsolutePath(); + envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library", "Bin" ).toString() + ";" + envs.get( "Path" ) ); + } + // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); + if ( builder.command( cmd ).start().waitFor() != 0 ) + throw new RuntimeException(); + } + /** * Returns Conda version as a {@code String}. * @@ -873,13 +914,27 @@ public List< String > getEnvironmentNames() throws IOException } public static boolean checkDependenciesInEnv(String envDir, List dependencies) { - if (!(new File(envDir).isDirectory())) + File envFile = new File(envDir); + if (!envFile.isDirectory()) return false; - Builder env = new Builder().conda(new File(envDir)); + runPythonIn(envFile, ); // TODO run conda list -p /full/path/to/env return false; } + public static boolean checkDependencyInEnv(String envDir, String dependencies, String version) { + File envFile = new File(envDir); + if (!envFile.isDirectory()) + return false; + String checkDepCode = ""; + try { + runPythonIn(envFile, checkDepCode); + } catch (IOException | InterruptedException e) { + return false; + } + return true; + } + public boolean checkEnvFromYamlExists(String envYaml) { if (envYaml == null || new File(envYaml).isFile() == false || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { From 2792d752f67cf3763d589495c9a8c8819535b0f5 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 23 Jan 2024 20:22:04 +0100 Subject: [PATCH 027/120] keep adding methods that improve the env management --- src/main/java/org/apposed/appose/Conda.java | 36 ++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index da91164..b5efc63 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -913,23 +913,43 @@ public List< String > getEnvironmentNames() throws IOException return envs; } - public static boolean checkDependenciesInEnv(String envDir, List dependencies) { + public static boolean checkAllDependenciesInEnv(String envDir, List dependencies) { + return checkUninstalledDependenciesInEnv(envDir, dependencies).size() == 0; + } + + public static List checkUninstalledDependenciesInEnv(String envDir, List dependencies) { File envFile = new File(envDir); if (!envFile.isDirectory()) - return false; - runPythonIn(envFile, ); - // TODO run conda list -p /full/path/to/env - return false; + return dependencies; + List uninstalled = dependencies.stream().filter(dep -> { + int ind = dep.indexOf("="); + if (ind == -1) return checkDependencyInEnv(envDir, dep); + String packName = dep.substring(0, ind); + String vv = dep.substring(ind + 1); + return checkDependencyInEnv(envDir, dep, vv); + }).collect(Collectors.toList()); + return uninstalled; } - public static boolean checkDependencyInEnv(String envDir, String dependencies, String version) { + public static boolean checkDependencyInEnv(String envDir, String dependency) { + return checkDependencyInEnv(envDir, dependency, null); + } + + public static boolean checkDependencyInEnv(String envDir, String dependency, String version) { File envFile = new File(envDir); if (!envFile.isDirectory()) return false; - String checkDepCode = ""; + String checkDepCode; + if ( version == null) { + checkDepCode = "import importlib, sys; pkg = %s; desired_version = %s; spec = importlib.util.find_spec(pkg); sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, version); + } else { + checkDepCode = "import importlib, sys; sys.exit(0) if importlib.util.find_spec(%s) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency); + } try { runPythonIn(envFile, checkDepCode); - } catch (IOException | InterruptedException e) { + } catch (RuntimeException | IOException | InterruptedException e) { return false; } return true; From ebf7abef03b56b369ab3911e3468a03e4eaf569f Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 23 Jan 2024 20:39:14 +0100 Subject: [PATCH 028/120] add todo --- src/main/java/org/apposed/appose/Conda.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index b5efc63..6b0f153 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -926,7 +926,7 @@ public static List checkUninstalledDependenciesInEnv(String envDir, Lis if (ind == -1) return checkDependencyInEnv(envDir, dep); String packName = dep.substring(0, ind); String vv = dep.substring(ind + 1); - return checkDependencyInEnv(envDir, dep, vv); + return checkDependencyInEnv(envDir, packName, vv); }).collect(Collectors.toList()); return uninstalled; } @@ -955,12 +955,16 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str return true; } + /** + * TODO figure out whether to use a dependency or not to parse the yaml file + * @param envYaml + * @return + */ public boolean checkEnvFromYamlExists(String envYaml) { if (envYaml == null || new File(envYaml).isFile() == false || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { return false; } - // TODO parse yaml without adding deps return false; } From 05f4c6d5d75fae7d253a39570fb894c9f94e1035 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 24 Jan 2024 15:47:19 +0100 Subject: [PATCH 029/120] improve the robustness of package checking --- src/main/java/org/apposed/appose/Conda.java | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 6b0f153..cad6016 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -936,16 +936,32 @@ public static boolean checkDependencyInEnv(String envDir, String dependency) { } public static boolean checkDependencyInEnv(String envDir, String dependency, String version) { + return checkDependencyInEnv(envDir, dependency, version, version); + } + + public static boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { File envFile = new File(envDir); if (!envFile.isDirectory()) return false; String checkDepCode; - if ( version == null) { + if (minversion != null && maxversion != null && minversion.equals(maxversion)) { checkDepCode = "import importlib, sys; pkg = %s; desired_version = %s; spec = importlib.util.find_spec(pkg); sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, version); - } else { + checkDepCode = String.format(checkDepCode, dependency, maxversion); + } else if (minversion == null && maxversion == null) { checkDepCode = "import importlib, sys; sys.exit(0) if importlib.util.find_spec(%s) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency); + } else if (maxversion == null) { + checkDepCode = "import importlib, sys; " + + "pkg = '%s'; desired_version = '%s'; " + + "spec = importlib.util.find_spec(pkg); " + + "sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, minversion); + } else { + checkDepCode = "import importlib, sys; " + + "pkg = '%s'; desired_version = '%s'; " + + "spec = importlib.util.find_spec(pkg); " + + "sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, maxversion); } try { runPythonIn(envFile, checkDepCode); From d46b8fa8965926dbcad7183462fc28f1670eb79c Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 24 Jan 2024 16:55:44 +0100 Subject: [PATCH 030/120] keep increasing robustness --- src/main/java/org/apposed/appose/Conda.java | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index cad6016..87d10b2 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -945,23 +945,40 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str return false; String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { - checkDepCode = "import importlib, sys; pkg = %s; desired_version = %s; spec = importlib.util.find_spec(pkg); sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + checkDepCode = "import importlib, sys; " + + "from importlib.metadata import version; " + + "from packaging import version as vv; " + + "pkg = %s; wanted_v = %s; " + + "spec = importlib.util.find_spec(pkg); " + + "sys.exit(0) if spec and vv.parse(version(pkg)) == vv.parse(wanted_v) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, maxversion); } else if (minversion == null && maxversion == null) { checkDepCode = "import importlib, sys; sys.exit(0) if importlib.util.find_spec(%s) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency); } else if (maxversion == null) { checkDepCode = "import importlib, sys; " + + "from importlib.metadata import version; " + + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + + "sys.exit(0) if spec and vv.parse(version(pkg)) >= vv.parse(desired_version) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, minversion); - } else { + } else if (minversion == null) { checkDepCode = "import importlib, sys; " + + "from importlib.metadata import version; " + + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and spec.version == desired_version else sys.exit(1)"; + + "sys.exit(0) if spec and vv.parse(version(pkg)) <= vv.parse(desired_version) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, maxversion); + } else { + checkDepCode = "import importlib, sys; " + + "from importlib.metadata import version; " + + "from packaging import version as vv; " + + "pkg = '%s'; min_v = '%s'; max_v = '%s'; " + + "spec = importlib.util.find_spec(pkg); " + + "sys.exit(0) if spec and vv.parse(version(pkg)) >= vv.parse(min_v) and vv.parse(version(pkg)) <= vv.parse(max_v) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, minversion, maxversion); } try { runPythonIn(envFile, checkDepCode); From 157211124f10e21ef5fd71e0032201b5c930ff66 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 24 Jan 2024 18:08:55 +0100 Subject: [PATCH 031/120] improve the logic of dependency checking --- src/main/java/org/apposed/appose/Conda.java | 73 ++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 87d10b2..18a4209 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -932,14 +932,71 @@ public static List checkUninstalledDependenciesInEnv(String envDir, Lis } public static boolean checkDependencyInEnv(String envDir, String dependency) { - return checkDependencyInEnv(envDir, dependency, null); + if (dependency.contains("==")) { + int ind = dependency.indexOf("=="); + return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim()); + } else if (dependency.contains(">=") && dependency.contains("<=") && dependency.contains(",")) { + int commaInd = dependency.indexOf(","); + int highInd = dependency.indexOf(">="); + int lowInd = dependency.indexOf("<="); + int minInd = Math.min(Math.min(commaInd, lowInd), highInd); + String packName = dependency.substring(0, minInd).trim(); + String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); + String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); + return checkDependencyInEnv(envDir, packName, minV, maxV, false); + } else if (dependency.contains(">=") && dependency.contains("<") && dependency.contains(",")) { + int commaInd = dependency.indexOf(","); + int highInd = dependency.indexOf(">="); + int lowInd = dependency.indexOf("<"); + int minInd = Math.min(Math.min(commaInd, lowInd), highInd); + String packName = dependency.substring(0, minInd).trim(); + String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); + String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); + return checkDependencyInEnv(envDir, packName, minV, null, false) && checkDependencyInEnv(envDir, packName, null, maxV, true); + } else if (dependency.contains(">") && dependency.contains("<=") && dependency.contains(",")) { + int commaInd = dependency.indexOf(","); + int highInd = dependency.indexOf(">"); + int lowInd = dependency.indexOf("<="); + int minInd = Math.min(Math.min(commaInd, lowInd), highInd); + String packName = dependency.substring(0, minInd).trim(); + String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); + String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); + return checkDependencyInEnv(envDir, packName, minV, null, true) && checkDependencyInEnv(envDir, packName, null, maxV, false); + } else if (dependency.contains(">") && dependency.contains("<") && dependency.contains(",")) { + int commaInd = dependency.indexOf(","); + int highInd = dependency.indexOf(">"); + int lowInd = dependency.indexOf(">"); + int minInd = Math.min(Math.min(commaInd, lowInd), highInd); + String packName = dependency.substring(0, minInd).trim(); + String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); + String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); + return checkDependencyInEnv(envDir, packName, minV, maxV, true); + } else if (dependency.contains(">")) { + int ind = dependency.indexOf(">"); + return checkDependencyInEnv(envDir, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), true); + } else if (dependency.contains(">=")) { + int ind = dependency.indexOf(">="); + return checkDependencyInEnv(envDir, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), false); + } else if (dependency.contains("<=")) { + int ind = dependency.indexOf("<="); + return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), null, false); + } else if (dependency.contains("<")) { + int ind = dependency.indexOf("<"); + return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim(), null, true); + } else { + return checkDependencyInEnv(envDir, dependency, null); + } } public static boolean checkDependencyInEnv(String envDir, String dependency, String version) { - return checkDependencyInEnv(envDir, dependency, version, version); + return checkDependencyInEnv(envDir, dependency, version, version, true); } public static boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { + return checkDependencyInEnv(envDir, dependency, minversion, maxversion, true); + } + + public static boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { File envFile = new File(envDir); if (!envFile.isDirectory()) return false; @@ -961,24 +1018,24 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) >= vv.parse(desired_version) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, minversion); + + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, minversion, strictlyBiggerOrSmaller ? ">" : ">="); } else if (minversion == null) { checkDepCode = "import importlib, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) <= vv.parse(desired_version) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, maxversion); + + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); } else { checkDepCode = "import importlib, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = '%s'; min_v = '%s'; max_v = '%s'; " + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) >= vv.parse(min_v) and vv.parse(version(pkg)) <= vv.parse(max_v) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, minversion, maxversion); + + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(min_v) and vv.parse(version(pkg)) %s vv.parse(max_v) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, dependency, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); } try { runPythonIn(envFile, checkDepCode); From bcbbe575961d927f467e8a8d72e06adb13694cdc Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Wed, 24 Jan 2024 18:23:55 +0100 Subject: [PATCH 032/120] add method to find whether python is installed and its version is correct --- src/main/java/org/apposed/appose/Conda.java | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Conda.java index 18a4209..c18ac76 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Conda.java @@ -1000,6 +1000,7 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str File envFile = new File(envDir); if (!envFile.isDirectory()) return false; + if (dependency.trim().equals("python")) return checkPythonInstallation(envDir, minversion, maxversion, strictlyBiggerOrSmaller); String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { checkDepCode = "import importlib, sys; " @@ -1045,6 +1046,37 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str return true; } + private static boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { + File envFile = new File(envDir); + String checkDepCode; + if (minversion != null && maxversion != null && minversion.equals(maxversion)) { + checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(version(pkg)) == vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, maxversion); + } else if (minversion == null && maxversion == null) { + return true; + } else if (maxversion == null) { + checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(version(platform.python_version())) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, minversion, strictlyBiggerOrSmaller ? ">" : ">="); + } else if (minversion == null) { + checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(version(platform.python_version())) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); + } else { + checkDepCode = "import platform; " + + "from packaging import version as vv; min_v = '%s'; max_v = '%s'; " + + "sys.exit(0) if vv.parse(platform.python_version()) %s vv.parse(min_v) and vv.parse(platform.python_version()) %s vv.parse(max_v) else sys.exit(1)"; + checkDepCode = String.format(checkDepCode, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); + } + try { + runPythonIn(envFile, checkDepCode); + } catch (RuntimeException | IOException | InterruptedException e) { + return false; + } + return true; + } + /** * TODO figure out whether to use a dependency or not to parse the yaml file * @param envYaml From a8dedd3237ba137d43563c7df3b1d159bf6555f2 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 29 Jan 2024 17:03:31 +0100 Subject: [PATCH 033/120] refactor Conda to Mamba to avoid confusion --- src/main/java/org/apposed/appose/Builder.java | 2 +- .../apposed/appose/{Conda.java => Mamba.java} | 114 ++++++++++++++---- .../apposed/appose/MambaInstallerUtils.java | 2 +- 3 files changed, 93 insertions(+), 25 deletions(-) rename src/main/java/org/apposed/appose/{Conda.java => Mamba.java} (91%) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 343023f..9562583 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -50,7 +50,7 @@ public Environment build() { // - Populate ${baseDirectory}/jars with Maven artifacts? try { - Conda conda = new Conda(Conda.BASE_PATH); + Mamba conda = new Mamba(Mamba.BASE_PATH); String envName = "appose"; if (conda.getEnvironmentNames().contains( envName )) { // TODO: Should we update it? For now, we just use it. diff --git a/src/main/java/org/apposed/appose/Conda.java b/src/main/java/org/apposed/appose/Mamba.java similarity index 91% rename from src/main/java/org/apposed/appose/Conda.java rename to src/main/java/org/apposed/appose/Mamba.java index c18ac76..fecd9b7 100644 --- a/src/main/java/org/apposed/appose/Conda.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -40,7 +40,6 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.nio.channels.Selector; import java.nio.file.Files; import java.nio.file.Paths; import java.text.SimpleDateFormat; @@ -54,16 +53,16 @@ import java.util.stream.Collectors; /** - * Conda environment manager, implemented by delegating to micromamba. + * Python environment manager, implemented by delegating to micromamba. * * @author Ko Sugawara * @author Curtis Rueden */ -public class Conda { +public class Mamba { final static String PYTHON_COMMAND = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; - final String condaCommand; + final String mambaCommand; private String envName = DEFAULT_ENVIRONMENT_NAME; @@ -73,7 +72,7 @@ public class Conda { public final static String DEFAULT_ENVIRONMENT_NAME = "base"; - private final static String CONDA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? + private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" : File.separator + "bin" + File.separator + "micromamba"; @@ -145,11 +144,80 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) * @throws ArchiveException * @throws URISyntaxException */ - public Conda() throws IOException, InterruptedException, ArchiveException, URISyntaxException + public Mamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { this(BASE_PATH); } + /** + * Create a new Conda object. The root dir for Conda installation can be + * specified as {@code String}. If there is no directory found at the specified + * path, Miniconda will be automatically installed in the path. It is expected + * that the Conda installation has executable commands as shown below: + * + *
+	 * CONDA_ROOT
+	 * ├── bin
+	 * │   ├── micromamba(.exe)
+	 * │   ... 
+	 * ├── envs
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
+	 * 
+ * + * @param rootdir + * The root dir for Mamba installation. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + * @throws ArchiveException + * @throws URISyntaxException + */ + public Mamba( final String rootdir, boolean installIfMissing) throws IOException, InterruptedException, ArchiveException, URISyntaxException + { + if (rootdir == null) + this.rootdir = BASE_PATH; + else + this.rootdir = rootdir; + this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH; + this.envsdir = this.rootdir + File.separator + "envs"; + if ( Files.notExists( Paths.get( mambaCommand ) ) ) + { + + final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); + tempFile.deleteOnExit(); + URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); + tempTarFile.deleteOnExit(); + MambaInstallerUtils.unBZip2(tempFile, tempTarFile); + File mambaBaseDir = new File(rootdir); + if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) + throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); + MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); + if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) + throw new IOException("Failed to create Micromamba default envs directory " + envsdir); + + } + + // The following command will throw an exception if Conda does not work as + // expected. + boolean executableSet = new File(mambaCommand).setExecutable(true); + if (!executableSet) + throw new IOException("Cannot set file as executable due to missing permissions, " + + "please do it manually: " + mambaCommand); + + // The following command will throw an exception if Conda does not work as + // expected. + getVersion(); + } + /** * Create a new Conda object. The root dir for Conda installation can be * specified as {@code String}. If there is no directory found at the specified @@ -177,15 +245,15 @@ public Conda() throws IOException, InterruptedException, ArchiveException, URISy * @throws ArchiveException * @throws URISyntaxException */ - public Conda( final String rootdir ) throws IOException, InterruptedException, ArchiveException, URISyntaxException + public Mamba( final String rootdir ) throws IOException, InterruptedException, ArchiveException, URISyntaxException { if (rootdir == null) this.rootdir = BASE_PATH; else this.rootdir = rootdir; - this.condaCommand = this.rootdir + CONDA_RELATIVE_PATH; + this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH; this.envsdir = this.rootdir + File.separator + "envs"; - if ( Files.notExists( Paths.get( condaCommand ) ) ) + if ( Files.notExists( Paths.get( mambaCommand ) ) ) { final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); @@ -209,10 +277,10 @@ public Conda( final String rootdir ) throws IOException, InterruptedException, A // The following command will throw an exception if Conda does not work as // expected. - boolean executableSet = new File(condaCommand).setExecutable(true); + boolean executableSet = new File(mambaCommand).setExecutable(true); if (!executableSet) throw new IOException("Cannot set file as executable due to missing permissions, " - + "please do it manually: " + condaCommand); + + "please do it manually: " + mambaCommand); // The following command will throw an exception if Conda does not work as // expected. @@ -278,7 +346,7 @@ public void updateIn( final String envName, final String... args ) throws IOExce { final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-y", "-n", envName ) ); cmd.addAll( Arrays.asList( args ) ); - runConda( cmd.stream().toArray( String[]::new ) ); + runMamba( cmd.stream().toArray( String[]::new ) ); } /** @@ -345,7 +413,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runConda( "env", "create", "--prefix", + runMamba( "env", "create", "--prefix", envsdir + File.separator + envName, "-f", envYaml, "-y" ); } @@ -375,7 +443,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runConda(consumer, "env", "create", "--prefix", + runMamba(consumer, "env", "create", "--prefix", envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); } @@ -416,7 +484,7 @@ public void create( final String envName, final boolean isForceCreation ) throws { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runConda( "create", "-y", "-p", envsdir + File.separator + envName ); + runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); } /** @@ -466,12 +534,12 @@ public void create( final String envName, final boolean isForceCreation, final S throw new EnvironmentExistsException(); final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); - runConda( cmd.stream().toArray( String[]::new ) ); + runMamba( cmd.stream().toArray( String[]::new ) ); } /** * This method works as if the user runs {@code conda activate envName}. This - * method internally calls {@link Conda#setEnvName(String)}. + * method internally calls {@link Mamba#setEnvName(String)}. * * @param envName * The environment name to be activated. @@ -558,7 +626,7 @@ public void installIn( final String envName, final String... args ) throws IOExc { final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-n", envName ) ); cmd.addAll( Arrays.asList( args ) ); - runConda( cmd.stream().toArray( String[]::new ) ); + runMamba( cmd.stream().toArray( String[]::new ) ); } /** @@ -718,7 +786,7 @@ public static void runPythonIn( final File envFile, final String... args ) throw public String getVersion() throws IOException, InterruptedException { final List< String > cmd = getBaseCommand(); - cmd.addAll( Arrays.asList( condaCommand, "--version" ) ); + cmd.addAll( Arrays.asList( mambaCommand, "--version" ) ); final Process process = getBuilder( false ).command( cmd ).start(); if ( process.waitFor() != 0 ) throw new RuntimeException(); @@ -739,12 +807,12 @@ public String getVersion() throws IOException, InterruptedException * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void runConda(Consumer consumer, final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(Consumer consumer, final String... args ) throws RuntimeException, IOException, InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); final List< String > cmd = getBaseCommand(); - cmd.add( condaCommand ); + cmd.add( mambaCommand ); cmd.addAll( Arrays.asList( args ) ); ProcessBuilder builder = getBuilder(false).command(cmd); @@ -826,10 +894,10 @@ public void runConda(Consumer consumer, final String... args ) throws Ru * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void runConda(final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException { final List< String > cmd = getBaseCommand(); - cmd.add( condaCommand ); + cmd.add( mambaCommand ); cmd.addAll( Arrays.asList( args ) ); if ( getBuilder( true ).command( cmd ).start().waitFor() != 0 ) throw new RuntimeException(); diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java index b64749c..c4e520b 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -122,7 +122,7 @@ public static void unTar(final File inputFile, final File outputDir) throws File } public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException { - String url = Conda.MICROMAMBA_URL; + String url = Mamba.MICROMAMBA_URL; final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); tempFile.deleteOnExit(); URL website = MambaInstallerUtils.redirectedURL(new URL(url)); From ffc3eddbd5d9cbcf9e9501c1583aacd0a164bcfa Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 29 Jan 2024 18:09:12 +0100 Subject: [PATCH 034/120] add exception --- .../apposed/appose/MambaInstallException.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/org/apposed/appose/MambaInstallException.java diff --git a/src/main/java/org/apposed/appose/MambaInstallException.java b/src/main/java/org/apposed/appose/MambaInstallException.java new file mode 100644 index 0000000..33f856e --- /dev/null +++ b/src/main/java/org/apposed/appose/MambaInstallException.java @@ -0,0 +1,28 @@ +package org.apposed.appose; + +/** + * Exception to be thrown when Micromamba is not found in the wanted directory + * + * @author Carlos Javier Garcia Lopez de Haro + */ +public class MambaInstallException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message + */ + public MambaInstallException() { + super("Micromamba installation not found in the provided directory."); + } + + /** + * Constructs a new exception with the specified detail message + * @param message + * the detail message. + */ + public MambaInstallException(String message) { + super(message); + } + +} From 599d3c04654a60c0456389ed80e41db74416d167 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 29 Jan 2024 18:22:42 +0100 Subject: [PATCH 035/120] give the possibility of installing or not when instantiating Mamba --- src/main/java/org/apposed/appose/Mamba.java | 164 ++++++++++++-------- 1 file changed, 99 insertions(+), 65 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index fecd9b7..62b47af 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -60,31 +60,68 @@ */ public class Mamba { - final static String PYTHON_COMMAND = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; - + /** + * String containing the path that points to the micromamba executable + */ final String mambaCommand; - - private String envName = DEFAULT_ENVIRONMENT_NAME; - + /** + * Name of the environment where the changes are going to be applied + */ + private String envName; + /** + * Root directory of micromamba that also contains the environments folder + * + *
+	 * rootdir
+	 * ├── bin
+	 * │   ├── micromamba(.exe)
+	 * │   ... 
+	 * ├── envs
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
+	 * 
+ */ private final String rootdir; - + /** + * Path to the folder that contains the directories + */ private final String envsdir; - + /* + * Path to Python executable from the environment directory + */ + final static String PYTHON_COMMAND = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; + /** + * Default name for a Python environment + */ public final static String DEFAULT_ENVIRONMENT_NAME = "base"; - + /** + * Relative path to the micromamba executable from the micromamba {@link #rootdir} + */ private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" : File.separator + "bin" + File.separator + "micromamba"; - + /** + * Path where Appose installs Micromamba by default + */ final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); - + /** + * Name of the folder inside the {@link #rootdir} that contains the different Python environments created by the Appose Micromamba + */ final public static String ENVS_NAME = "envs"; - + /** + * URL from where Micromamba is downloaded to be installed + */ public final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; - + /** + * ID used to identify the text retrieved from the error stream when a consumer is used + */ public final static String ERR_STREAM_UUUID = UUID.randomUUID().toString(); + /** + * + * @return a String that identifies the current OS to download the correct Micromamba version + */ private static String microMambaPlatform() { String osName = System.getProperty("os.name"); if (osName.startsWith("Windows")) osName = "Windows"; @@ -119,16 +156,50 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) } /** - * Create a new Conda object. The root dir for the Micromamba installation - * will be /user/.local/share/appose/micromamba. - * If there is no directory found at the specified - * path, Miniconda will be automatically installed in the path. It is expected - * that the Conda installation has executable commands as shown below: + * Create a new {@link Mamba} object. The root dir for the Micromamba installation + * will be the default base path defined at {@link BASE_PATH} + * If there is no Micromamba found at the specified + * path, a {@link MambaInstallException} will be thrown + * + * It is expected that the Micromamba installation has executable commands as shown below: * *
-	 * CONDA_ROOT
-	 * ├── condabin
-	 * │   ├── conda(.bat)
+	 * MAMBA_ROOT
+	 * ├── bin
+	 * │   ├── micromamba(.exe)
+	 * │   ... 
+	 * ├── envs
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
+	 * 
+ * + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + * @throws ArchiveException + * @throws URISyntaxException + * @throws MambaInstallException if Micromamba has not been already installed in the default path {@link #BASE_PATH} + */ + public Mamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException, MambaInstallException + { + this(BASE_PATH, false); + } + + /** + * Create a new {@link Mamba} object. The root dir for the Micromamba installation + * will be the default base path defined at {@link BASE_PATH} + * If there is no Micromamba found at the specified + * path, it will be installed automatically. + * + * It is expected that the Micromamba installation has executable commands as shown below: + * + *
+	 * MAMBA_ROOT
+	 * ├── bin
+	 * │   ├── micromamba(.exe)
 	 * │   ... 
 	 * ├── envs
 	 * │   ├── your_env
@@ -144,9 +215,9 @@ private ProcessBuilder getBuilder( final boolean isInheritIO )
 	 * @throws ArchiveException 
 	 * @throws URISyntaxException 
 	 */
-	public Mamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException
+	public Mamba(boolean installIfNeeded) throws IOException, InterruptedException, ArchiveException, URISyntaxException
 	{
-		this(BASE_PATH);
+		this(BASE_PATH, installIfNeeded);
 	}
 
 	/**
@@ -176,46 +247,9 @@ public Mamba() throws IOException, InterruptedException, ArchiveException, URISy
 	 * @throws ArchiveException 
 	 * @throws URISyntaxException 
 	 */
-	public Mamba( final String rootdir, boolean installIfMissing) throws IOException, InterruptedException, ArchiveException, URISyntaxException
+	public Mamba( final String rootdir) throws IOException, InterruptedException, ArchiveException, URISyntaxException
 	{
-		if (rootdir == null)
-			this.rootdir = BASE_PATH;
-		else
-			this.rootdir = rootdir;
-		this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH;
-		this.envsdir = this.rootdir + File.separator + "envs";
-		if ( Files.notExists( Paths.get( mambaCommand ) ) )
-		{
-
-			final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" );
-			tempFile.deleteOnExit();
-			URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL));
-			ReadableByteChannel rbc = Channels.newChannel(website.openStream());
-			try (FileOutputStream fos = new FileOutputStream(tempFile)) {
-				fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
-			}
-			final File tempTarFile = File.createTempFile( "miniconda", ".tar" );
-			tempTarFile.deleteOnExit();
-			MambaInstallerUtils.unBZip2(tempFile, tempTarFile);
-			File mambaBaseDir = new File(rootdir);
-			if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs())
-    	        throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath());
-			MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir);
-			if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs())
-    	        throw new IOException("Failed to create Micromamba default envs directory " + envsdir);
-			
-		}
-
-		// The following command will throw an exception if Conda does not work as
-		// expected.
-		boolean executableSet = new File(mambaCommand).setExecutable(true);
-		if (!executableSet)
-			throw new IOException("Cannot set file as executable due to missing permissions, "
-					+ "please do it manually: " + mambaCommand);
-		
-		// The following command will throw an exception if Conda does not work as
-		// expected.
-		getVersion();
+		this(rootdir, false);
 	}
 
 	/**
@@ -226,8 +260,8 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException
 	 * 
 	 * 
 	 * CONDA_ROOT
-	 * ├── condabin
-	 * │   ├── conda(.bat)
+	 * ├── bin
+	 * │   ├── micromamba(.exe)
 	 * │   ... 
 	 * ├── envs
 	 * │   ├── your_env
@@ -235,7 +269,7 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException
 	 * 
* * @param rootdir - * The root dir for Conda installation. + * The root dir for Mamba installation. * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -245,7 +279,7 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException * @throws ArchiveException * @throws URISyntaxException */ - public Mamba( final String rootdir ) throws IOException, InterruptedException, ArchiveException, URISyntaxException + public Mamba( final String rootdir, boolean installIfMissing) throws IOException, InterruptedException, ArchiveException, URISyntaxException { if (rootdir == null) this.rootdir = BASE_PATH; From bcbb4cf6d77228994b38b292c2cd56fd329533dc Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 29 Jan 2024 19:22:12 +0100 Subject: [PATCH 036/120] reorganize method to make it more understandable --- src/main/java/org/apposed/appose/Mamba.java | 122 +++++++++++++------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 62b47af..296018b 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -158,8 +158,7 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) /** * Create a new {@link Mamba} object. The root dir for the Micromamba installation * will be the default base path defined at {@link BASE_PATH} - * If there is no Micromamba found at the specified - * path, a {@link MambaInstallException} will be thrown + * If there is no Micromamba found at the base path {@link BASE_PATH}, a {@link MambaInstallException} will be thrown * * It is expected that the Micromamba installation has executable commands as shown below: * @@ -181,9 +180,11 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) * thrown. * @throws ArchiveException * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been already installed in the default path {@link #BASE_PATH} + * @throws MambaInstallException if Micromamba has not been installed in the default path {@link #BASE_PATH} */ - public Mamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException, MambaInstallException + public Mamba() throws IOException, + InterruptedException, ArchiveException, + URISyntaxException, MambaInstallException { this(BASE_PATH, false); } @@ -192,7 +193,8 @@ public Mamba() throws IOException, InterruptedException, ArchiveException, URISy * Create a new {@link Mamba} object. The root dir for the Micromamba installation * will be the default base path defined at {@link BASE_PATH} * If there is no Micromamba found at the specified - * path, it will be installed automatically. + * path, it will be installed automatically if the parameter 'installIfNeeded' + * is true. If not a {@link MambaInstallException} will be thrown. * * It is expected that the Micromamba installation has executable commands as shown below: * @@ -206,6 +208,10 @@ public Mamba() throws IOException, InterruptedException, ArchiveException, URISy * │   │   ├── python(.exe) *
* + * @param installIfNeeded + * if Micormamba is not installed in the default dir {@link #BASE_PATH}, Appose installs it + * automatically + * * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -214,20 +220,25 @@ public Mamba() throws IOException, InterruptedException, ArchiveException, URISy * thrown. * @throws ArchiveException * @throws URISyntaxException + * @throws MambaInstallException if Micromamba has not been installed in the default path {@link #BASE_PATH} + * and 'installIfNeeded' is false */ - public Mamba(boolean installIfNeeded) throws IOException, InterruptedException, ArchiveException, URISyntaxException + public Mamba(boolean installIfNeeded) throws IOException, + InterruptedException, ArchiveException, + URISyntaxException, MambaInstallException { this(BASE_PATH, installIfNeeded); } /** - * Create a new Conda object. The root dir for Conda installation can be - * specified as {@code String}. If there is no directory found at the specified - * path, Miniconda will be automatically installed in the path. It is expected - * that the Conda installation has executable commands as shown below: + * Create a new Mamba object. The root dir for Mamba installation can be + * specified as {@code String}. + * If there is no Micromamba found at the specified path, a {@link MambaInstallException} will be thrown. + * + * It is expected that the Micromamba installation has executable commands as shown below: * *
-	 * CONDA_ROOT
+	 * MAMBA_ROOT
 	 * ├── bin
 	 * │   ├── micromamba(.exe)
 	 * │   ... 
@@ -246,20 +257,25 @@ public Mamba(boolean installIfNeeded) throws IOException, InterruptedException,
 	 *             thrown.
 	 * @throws ArchiveException 
 	 * @throws URISyntaxException 
+	 * @throws MambaInstallException if Micromamba has not been installed in the provided 'rootdir' path
 	 */
-	public Mamba( final String rootdir) throws IOException, InterruptedException, ArchiveException, URISyntaxException
+	public Mamba( final String rootdir) throws IOException, 
+												InterruptedException, ArchiveException, 
+												URISyntaxException, MambaInstallException
 	{
 		this(rootdir, false);
 	}
 
 	/**
 	 * Create a new Conda object. The root dir for Conda installation can be
-	 * specified as {@code String}. If there is no directory found at the specified
-	 * path, Miniconda will be automatically installed in the path. It is expected
-	 * that the Conda installation has executable commands as shown below:
+	 * specified as {@code String}. 
+	 * If there is no Micromamba found at the specified path, it will be installed automatically 
+	 * if the parameter 'installIfNeeded' is true. If not a {@link MambaInstallException} will be thrown.
+	 * 
+	 * It is expected that the Conda installation has executable commands as shown below:
 	 * 
 	 * 
-	 * CONDA_ROOT
+	 * MAMBA_ROOT
 	 * ├── bin
 	 * │   ├── micromamba(.exe)
 	 * │   ... 
@@ -269,7 +285,10 @@ public Mamba( final String rootdir) throws IOException, InterruptedException, Ar
 	 * 
* * @param rootdir - * The root dir for Mamba installation. + * The root dir for Mamba installation. + * @param installIfNeeded + * if Micormamba is not installed in the path specified by 'rootdir', Appose installs it + * automatically * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -278,36 +297,55 @@ public Mamba( final String rootdir) throws IOException, InterruptedException, Ar * thrown. * @throws ArchiveException * @throws URISyntaxException + * @throws MambaInstallException if Micromamba has not been installed in the dir defined by 'rootdir' + * and 'installIfNeeded' is false */ - public Mamba( final String rootdir, boolean installIfMissing) throws IOException, InterruptedException, ArchiveException, URISyntaxException + public Mamba( final String rootdir, boolean installIfMissing) throws IOException, + InterruptedException, ArchiveException, + URISyntaxException, MambaInstallException { if (rootdir == null) this.rootdir = BASE_PATH; else this.rootdir = rootdir; this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH; - this.envsdir = this.rootdir + File.separator + "envs"; - if ( Files.notExists( Paths.get( mambaCommand ) ) ) - { + this.envsdir = this.rootdir + File.separator + ENVS_NAME; + boolean filesExist = Files.notExists( Paths.get( mambaCommand ) ); + if (!filesExist && !installIfMissing) + throw new MambaInstallException(); + boolean installed = true; + try { + getVersion(); + } catch (RuntimeException ex) { + installed = false; + } + if (!installed && !installIfMissing) + throw new MambaInstallException("Even though Micromamba installation has been found, " + + "it did not work as expected. Re-installation is advised. Path of installation found: " + this.rootdir); + if (installed) + return; + installMicromamba(); + } + + private void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { - final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); - tempFile.deleteOnExit(); - URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); - ReadableByteChannel rbc = Channels.newChannel(website.openStream()); - try (FileOutputStream fos = new FileOutputStream(tempFile)) { - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } - final File tempTarFile = File.createTempFile( "miniconda", ".tar" ); - tempTarFile.deleteOnExit(); - MambaInstallerUtils.unBZip2(tempFile, tempTarFile); - File mambaBaseDir = new File(rootdir); - if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) - throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); - MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); - if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) - throw new IOException("Failed to create Micromamba default envs directory " + envsdir); - + final File tempFile = File.createTempFile( "micromamba", ".tar.bz2" ); + tempFile.deleteOnExit(); + URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); } + final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); + tempTarFile.deleteOnExit(); + MambaInstallerUtils.unBZip2(tempFile, tempTarFile); + File mambaBaseDir = new File(rootdir); + if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) + throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); + MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); + if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) + throw new IOException("Failed to create Micromamba default envs directory " + envsdir); + // The following command will throw an exception if Conda does not work as // expected. @@ -315,10 +353,6 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException if (!executableSet) throw new IOException("Cannot set file as executable due to missing permissions, " + "please do it manually: " + mambaCommand); - - // The following command will throw an exception if Conda does not work as - // expected. - getVersion(); } public String getEnvsDir() { @@ -748,13 +782,13 @@ public void runPythonIn( final String envName, final String... args ) throws IOE if ( envName.equals( DEFAULT_ENVIRONMENT_NAME ) ) cmd.add( PYTHON_COMMAND ); else - cmd.add( Paths.get( "envs", envName, PYTHON_COMMAND ).toString() ); + cmd.add( Paths.get( ENVS_NAME, envName, PYTHON_COMMAND ).toString() ); cmd.addAll( Arrays.asList( args ) ); final ProcessBuilder builder = getBuilder( true ); if ( SystemUtils.IS_OS_WINDOWS ) { final Map< String, String > envs = builder.environment(); - final String envDir = Paths.get( rootdir, "envs", envName ).toString(); + final String envDir = Paths.get( rootdir, ENVS_NAME, envName ).toString(); envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); From 4b6b3f96c9dd88b9e854c2fd331280d7ec8602bb Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 29 Jan 2024 19:33:42 +0100 Subject: [PATCH 037/120] keep increaseing robustness --- src/main/java/org/apposed/appose/Mamba.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 296018b..ae91af2 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -412,7 +412,7 @@ public void update( final String... args ) throws IOException, InterruptedExcept */ public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException { - final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-y", "-n", envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-y", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); runMamba( cmd.stream().toArray( String[]::new ) ); } From cbe14beed062b1b27035980a63b83fe05aca7652 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 00:13:40 +0100 Subject: [PATCH 038/120] delete unnecessary method --- src/main/java/org/apposed/appose/Mamba.java | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index ae91af2..63fa6bc 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -555,27 +555,6 @@ public void create( final String envName, final boolean isForceCreation ) throws runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); } - /** - * Run {@code conda create} to create a new conda environment with a list of - * specified packages. - * - * @param envName - * The environment name to be created. - * @param args - * The list of packages to be installed on environment creation and - * extra parameters as {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - public void create( final String envName, final String... args ) throws IOException, InterruptedException - { - create( envName, false, args ); - } - /** * Run {@code conda create} to create a new conda environment with a list of * specified packages. From 923817a973c92df6f2f2d2d83cadd5339c68ff54 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 00:43:03 +0100 Subject: [PATCH 039/120] improve robustness of methods --- src/main/java/org/apposed/appose/Mamba.java | 71 +++++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 63fa6bc..e30bf05 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -48,6 +48,7 @@ import java.util.Calendar; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -560,14 +561,16 @@ public void create( final String envName, final boolean isForceCreation ) throws * specified packages. * * @param envName - * The environment name to be created. + * The environment name to be created. CAnnot be null. * @param isForceCreation * Force creation of the environment if {@code true}. If this value * is {@code false} and an environment with the specified name * already exists, throw an {@link EnvironmentExistsException}. - * @param args - * The list of packages to be installed on environment creation and - * extra parameters as {@code String...}. + * @param channels + * the channels from where the packages can be installed. Can be null + * @param packages + * the packages that want to be installed during env creation. They can contain the version. + * For example, "python" or "python=3.10.1", "numpy" or "numpy=1.20.1". CAn be null if no packages want to be installed * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -575,12 +578,16 @@ public void create( final String envName, final boolean isForceCreation ) throws * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException + public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException { + Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); - cmd.addAll( Arrays.asList( args ) ); + if (channels == null) channels = new ArrayList(); + for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} + if (packages == null) packages = new ArrayList(); + for (String pack : packages) { cmd.add(pack);} runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -653,6 +660,56 @@ public void install( final String... args ) throws IOException, InterruptedExcep installIn( envName, args ); } + /** + * Run {@code conda install} in the activated environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param channels + * the channels from where the packages can be installed. Can be null + * @param packages + * the packages that want to be installed during env creation. They can contain the version. + * For example, "python" or "python=3.10.1", "numpy" or "numpy=1.20.1". CAn be null if no packages want to be installed + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void install( List channels, List packages ) throws IOException, InterruptedException + { + installIn( envName, channels, packages ); + } + + /** + * Run {@code conda install} in the specified environment. A list of packages to + * install and extra parameters can be specified as {@code args}. + * + * @param envName + * The environment name to be used for the install command. + * @param channels + * the channels from where the packages can be installed. Can be null + * @param packages + * the packages that want to be installed during env creation. They can contain the version. + * For example, "python" or "python=3.10.1", "numpy" or "numpy=1.20.1". CAn be null if no packages want to be installed + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void installIn( final String envName, List channels, List packages ) throws IOException, InterruptedException + { + Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) ); + if (channels == null) channels = new ArrayList(); + for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} + if (packages == null) packages = new ArrayList(); + for (String pack : packages) { cmd.add(pack);} + runMamba( cmd.stream().toArray( String[]::new ) ); + } + /** * Run {@code conda install} in the specified environment. A list of packages to * install and extra parameters can be specified as {@code args}. @@ -671,7 +728,7 @@ public void install( final String... args ) throws IOException, InterruptedExcep */ public void installIn( final String envName, final String... args ) throws IOException, InterruptedException { - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-n", envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); runMamba( cmd.stream().toArray( String[]::new ) ); } From a83b234577d8a4ce3a6008b2c414fc297480f9b8 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 10:43:56 +0100 Subject: [PATCH 040/120] make methods dynamic --- src/main/java/org/apposed/appose/Mamba.java | 52 +++++++++++---------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index e30bf05..1707fe9 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1085,28 +1085,29 @@ public List< String > getEnvironmentNames() throws IOException return envs; } - public static boolean checkAllDependenciesInEnv(String envDir, List dependencies) { - return checkUninstalledDependenciesInEnv(envDir, dependencies).size() == 0; + public boolean checkAllDependenciesInEnv(String envName, List dependencies) { + return checkUninstalledDependenciesInEnv(envName, dependencies).size() == 0; } - public static List checkUninstalledDependenciesInEnv(String envDir, List dependencies) { - File envFile = new File(envDir); - if (!envFile.isDirectory()) + public List checkUninstalledDependenciesInEnv(String envName, List dependencies) { + File envFile = new File(this.envsdir, envName); + File envFile2 = new File(envName); + if (!envFile.isDirectory() && !envFile2.isDirectory()) return dependencies; List uninstalled = dependencies.stream().filter(dep -> { int ind = dep.indexOf("="); - if (ind == -1) return checkDependencyInEnv(envDir, dep); + if (ind == -1) return checkDependencyInEnv(envName, dep); String packName = dep.substring(0, ind); String vv = dep.substring(ind + 1); - return checkDependencyInEnv(envDir, packName, vv); + return checkDependencyInEnv(envName, packName, vv); }).collect(Collectors.toList()); return uninstalled; } - public static boolean checkDependencyInEnv(String envDir, String dependency) { + public boolean checkDependencyInEnv(String envName, String dependency) { if (dependency.contains("==")) { int ind = dependency.indexOf("=="); - return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim()); + return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim()); } else if (dependency.contains(">=") && dependency.contains("<=") && dependency.contains(",")) { int commaInd = dependency.indexOf(","); int highInd = dependency.indexOf(">="); @@ -1115,7 +1116,7 @@ public static boolean checkDependencyInEnv(String envDir, String dependency) { String packName = dependency.substring(0, minInd).trim(); String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envDir, packName, minV, maxV, false); + return checkDependencyInEnv(envName, packName, minV, maxV, false); } else if (dependency.contains(">=") && dependency.contains("<") && dependency.contains(",")) { int commaInd = dependency.indexOf(","); int highInd = dependency.indexOf(">="); @@ -1124,7 +1125,7 @@ public static boolean checkDependencyInEnv(String envDir, String dependency) { String packName = dependency.substring(0, minInd).trim(); String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envDir, packName, minV, null, false) && checkDependencyInEnv(envDir, packName, null, maxV, true); + return checkDependencyInEnv(envName, packName, minV, null, false) && checkDependencyInEnv(envName, packName, null, maxV, true); } else if (dependency.contains(">") && dependency.contains("<=") && dependency.contains(",")) { int commaInd = dependency.indexOf(","); int highInd = dependency.indexOf(">"); @@ -1133,7 +1134,7 @@ public static boolean checkDependencyInEnv(String envDir, String dependency) { String packName = dependency.substring(0, minInd).trim(); String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envDir, packName, minV, null, true) && checkDependencyInEnv(envDir, packName, null, maxV, false); + return checkDependencyInEnv(envName, packName, minV, null, true) && checkDependencyInEnv(envName, packName, null, maxV, false); } else if (dependency.contains(">") && dependency.contains("<") && dependency.contains(",")) { int commaInd = dependency.indexOf(","); int highInd = dependency.indexOf(">"); @@ -1142,36 +1143,39 @@ public static boolean checkDependencyInEnv(String envDir, String dependency) { String packName = dependency.substring(0, minInd).trim(); String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envDir, packName, minV, maxV, true); + return checkDependencyInEnv(envName, packName, minV, maxV, true); } else if (dependency.contains(">")) { int ind = dependency.indexOf(">"); - return checkDependencyInEnv(envDir, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), true); + return checkDependencyInEnv(envName, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), true); } else if (dependency.contains(">=")) { int ind = dependency.indexOf(">="); - return checkDependencyInEnv(envDir, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), false); + return checkDependencyInEnv(envName, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), false); } else if (dependency.contains("<=")) { int ind = dependency.indexOf("<="); - return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), null, false); + return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), null, false); } else if (dependency.contains("<")) { int ind = dependency.indexOf("<"); - return checkDependencyInEnv(envDir, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim(), null, true); + return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim(), null, true); } else { - return checkDependencyInEnv(envDir, dependency, null); + return checkDependencyInEnv(envName, dependency, null); } } - public static boolean checkDependencyInEnv(String envDir, String dependency, String version) { + public boolean checkDependencyInEnv(String envDir, String dependency, String version) { return checkDependencyInEnv(envDir, dependency, version, version, true); } - public static boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { + public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { return checkDependencyInEnv(envDir, dependency, minversion, maxversion, true); } - public static boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { - File envFile = new File(envDir); - if (!envFile.isDirectory()) + public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { + File envFile = new File(this.envsdir, envName); + File envFile2 = new File(envName); + if (!envFile.isDirectory() && !envFile2.isDirectory()) return false; + else if (!envFile.isDirectory()) + envFile = envFile2; if (dependency.trim().equals("python")) return checkPythonInstallation(envDir, minversion, maxversion, strictlyBiggerOrSmaller); String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { @@ -1218,7 +1222,7 @@ public static boolean checkDependencyInEnv(String envDir, String dependency, Str return true; } - private static boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { + private boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { File envFile = new File(envDir); String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { From 1f6007cc1694dd843153e7d526554fb72b285ee6 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 11:23:54 +0100 Subject: [PATCH 041/120] add docs --- src/main/java/org/apposed/appose/Mamba.java | 105 ++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 1707fe9..a9900a3 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1085,10 +1085,37 @@ public List< String > getEnvironmentNames() throws IOException return envs; } + /** + * Check whether a list of dependencies provided is installed in the wanted environment. + * + * @param envName + * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * This parameter can also be the full path to an independent environment. + * @param dependencies + * The list of dependencies that should be installed in the environment. + * They can contain version requirements. The names should be the ones used to import the package inside python, + * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" + * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * @return true if the packages are installed or false otherwise + */ public boolean checkAllDependenciesInEnv(String envName, List dependencies) { return checkUninstalledDependenciesInEnv(envName, dependencies).size() == 0; } + /** + * Returns a list containing the packages that are not installed in the wanted environment + * from the list of dependencies provided + * + * @param envName + * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * This parameter can also be the full path to an independent environment. + * @param dependencies + * The list of dependencies that should be installed in the environment. + * They can contain version requirements. The names should be the ones used to import the package inside python, + * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" + * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * @return true if the packages are installed or false otherwise + */ public List checkUninstalledDependenciesInEnv(String envName, List dependencies) { File envFile = new File(this.envsdir, envName); File envFile2 = new File(envName); @@ -1104,6 +1131,19 @@ public List checkUninstalledDependenciesInEnv(String envName, List=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * @return true if the package is installed or false otherwise + */ public boolean checkDependencyInEnv(String envName, String dependency) { if (dependency.contains("==")) { int ind = dependency.indexOf("=="); @@ -1161,14 +1201,79 @@ public boolean checkDependencyInEnv(String envName, String dependency) { } } + /** + * Checks whether a package of a specific version is installed in the wanted environment. + * + * @param envName + * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * This parameter can also be the full path to an independent environment. + * @param dependencies + * The name of the package that should be installed in the env. The String should only contain the name, no version, + * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" + * or "sklearn", not "scikit-learn". + * @param version + * the specific version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0" + * @return true if the package is installed or false otherwise + */ public boolean checkDependencyInEnv(String envDir, String dependency, String version) { return checkDependencyInEnv(envDir, dependency, version, version, true); } + /** + * Checks whether a package with specific version constraints is installed in the wanted environment. + * In this method the minversion argument should be strictly smaller than the version of interest and + * the maxversion strictly bigger. + * This method checks that: dependency >minversion, =minversion, <=maxversion) look at the method + * {@link #checkDependencyInEnv(String, String, String, String, boolean)} with the lst parameter set to false. + * + * @param envName + * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * This parameter can also be the full path to an independent environment. + * @param dependencies + * The name of the package that should be installed in the env. The String should only contain the name, no version, + * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" + * or "sklearn", not "scikit-learn". + * @param minversion + * the minimum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". + * This version should be strictly smaller than the one of interest, if for example "1.9" is given, it is assumed that + * pacakge_version>1.9. + * If there is no minimum version requirement for the package of interest, set this argument to null. + * @param maxversion + * the maximum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". + * This version should be strictly bigger than the one of interest, if for example "1.9" is given, it is assumed that + * pacakge_version<1.9. + * If there is no maximum version requirement for the package of interest, set this argument to null. + * @return true if the package is installed or false otherwise + */ public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { return checkDependencyInEnv(envDir, dependency, minversion, maxversion, true); } + /** + * Checks whether a package with specific version constraints is installed in the wanted environment. + * Depending on the last argument ('strictlyBiggerOrSmaller') 'minversion' and 'maxversion' + * will be strictly bigger(>=) or smaller(<) or bigger or equal (>=) or smaller or equal<>=) + * In this method the minversion argument should be strictly smaller than the version of interest and + * the maxversion strictly bigger. + * + * @param envName + * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * This parameter can also be the full path to an independent environment. + * @param dependencies + * The name of the package that should be installed in the env. The String should only contain the name, no version, + * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" + * or "sklearn", not "scikit-learn". + * @param minversion + * the minimum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". + * If there is no minimum version requirement for the package of interest, set this argument to null. + * @param maxversion + * the maximum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". + * If there is no maximum version requirement for the package of interest, set this argument to null. + * @param strictlyBiggerOrSmaller + * Whether the minversion and maxversion shuld be strictly smaller and bigger or not + * @return true if the package is installed or false otherwise + */ public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { File envFile = new File(this.envsdir, envName); File envFile2 = new File(envName); From 4f320bdf412b3a73fa072d1d4523644e55d40a62 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 12:42:01 +0100 Subject: [PATCH 042/120] design consumer scaffold --- src/main/java/org/apposed/appose/Mamba.java | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index a9900a3..e599200 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -87,6 +87,25 @@ public class Mamba { * Path to the folder that contains the directories */ private final String envsdir; + /** + * Consumer that tracks the progress in the download of micromamba, the software used + * by this class to manage Python environments + */ + private Consumer mambaDnwldProgress; + /** + * Consumer that tracks the progress decompressing the downloaded micromamba files. + */ + private Consumer mambaDecompressProgress; + /** + * Consumer that tracks the console output produced by the micromamba process when it is executed. + * This consumer saves all the log of every micromamba execution + */ + private Consumer consoleConsumer; + /** + * Consumer that tracks the error output produced by the micromamba process when it is executed. + * This consumer saves all the log of every micromamba execution + */ + private Consumer errConsumer; /* * Path to Python executable from the environment directory */ @@ -328,6 +347,57 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException installMicromamba(); } + /** + * + * @return the progress downloading the micromamba software from the Interenet, useful to track a fresh micromamba installation + */ + public double getMicromambaDownloadProgress(){ + return this.mambaDnwldProgress; + } + + /** + * + * @returnthe progress decompressing the micromamba software downloaded from the Interenet, + * useful to track a fresh micromamba installation + */ + public double getMicromambaDecompressProgress(){ + return this.mambaDnwldProgress; + } + + /** + * + * @return all the console output produced by micromamba ever since the {@link Mamba} was instantiated + */ + public String getMicromambaConsoleStream(){ + return this.mambaConsoleOut; + } + + /** + * + * @return all the error output produced by micromamba ever since the {@link Mamba} was instantiated + */ + public String getMicromambaErrStream(){ + return mambaConsoleErr; + } + + /** + * Set a custom consumer for the console output of every micromamba call + * @param custom + * custom consumer that receives every console line outputed by ecery micromamba call + */ + public void setConsoleOutputConsumer(Consumer custom) { + this.customConsoleConsumer = custom; + } + + /** + * Set a custom consumer for the error output of every micromamba call + * @param custom + * custom consumer that receives every error line outputed by ecery micromamba call + */ + public void setErrorOutputConsumer(Consumer custom) { + this.customErrorConsumer = custom; + } + private void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { final File tempFile = File.createTempFile( "micromamba", ".tar.bz2" ); From d5aec8c0df6d542dc3c27e3febbdd659fad853a6 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 13:10:05 +0100 Subject: [PATCH 043/120] define the consumers and remove the argument from method --- src/main/java/org/apposed/appose/Mamba.java | 65 ++++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index e599200..f2663ec 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -87,25 +87,59 @@ public class Mamba { * Path to the folder that contains the directories */ private final String envsdir; + /** + * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. + * + */ + private double mambaDnwldProgress = 0.0; + /** + * Progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba + * software. VAlue between 0 and 1. + */ + private double mambaDecompressProgress = 0.0; /** * Consumer that tracks the progress in the download of micromamba, the software used * by this class to manage Python environments */ - private Consumer mambaDnwldProgress; + private Consumer mambaDnwldProgressConsumer; /** * Consumer that tracks the progress decompressing the downloaded micromamba files. */ - private Consumer mambaDecompressProgress; + private Consumer mambaDecompressProgressConsumer; + /** + * String that contains all the console output produced by micromamba ever since the {@link Mamba} was instantiated + */ + private String mambaConsoleOut = ""; + /** + * String that contains all the error output produced by micromamba ever since the {@link Mamba} was instantiated + */ + private String mambaConsoleErr = ""; + /** + * User custom consumer that tracks the console output produced by the micromamba process when it is executed. + */ + private Consumer customConsoleConsumer; + /** + * User custom consumer that tracks the error output produced by the micromamba process when it is executed. + */ + private Consumer customErrorConsumer; /** * Consumer that tracks the console output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer consoleConsumer; + private Consumer consoleConsumer = (str) -> { + mambaConsoleOut += str; + if (customConsoleConsumer != null) + customConsoleConsumer.accept(str); + }; /** * Consumer that tracks the error output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer errConsumer; + private Consumer errConsumer = (str) -> { + mambaConsoleErr += str; + if (customErrorConsumer != null) + customErrorConsumer.accept(str); + }; /* * Path to Python executable from the environment directory */ @@ -349,7 +383,7 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException /** * - * @return the progress downloading the micromamba software from the Interenet, useful to track a fresh micromamba installation + * @return the progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. */ public double getMicromambaDownloadProgress(){ return this.mambaDnwldProgress; @@ -357,11 +391,11 @@ public double getMicromambaDownloadProgress(){ /** * - * @returnthe progress decompressing the micromamba software downloaded from the Interenet, - * useful to track a fresh micromamba installation + * @returnthe the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba + * software. VAlue between 0 and 1. */ public double getMicromambaDecompressProgress(){ - return this.mambaDnwldProgress; + return this.mambaDecompressProgress; } /** @@ -981,8 +1015,9 @@ public String getVersion() throws IOException, InterruptedException * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void runMamba(Consumer consumer, final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException { + Thread mainThread = Thread.currentThread(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); final List< String > cmd = getBaseCommand(); @@ -992,7 +1027,7 @@ public void runMamba(Consumer consumer, final String... args ) throws Ru ProcessBuilder builder = getBuilder(false).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. - consumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); + this.consoleConsumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); long updatePeriod = 300; Thread outputThread = new Thread(() -> { try ( @@ -1007,6 +1042,7 @@ public void runMamba(Consumer consumer, final String... args ) throws Ru int newLineIndex; long t0 = System.currentTimeMillis(); while (process.isAlive() || inputStream.available() > 0) { + if (mainThread.isInterrupted()) return; if (inputStream.available() > 0) { processBuff.append(new String(buffer, 0, inputStream.read(buffer))); while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { @@ -1025,8 +1061,7 @@ public void runMamba(Consumer consumer, final String... args ) throws Ru // Sleep for a bit to avoid busy waiting Thread.sleep(60); if (System.currentTimeMillis() - t0 > updatePeriod) { - // TODO decide what to do with the err stream consumer.accept(errChunk.equals("") ? null : errChunk); - consumer.accept(processChunk); + this.consoleConsumer.accept(processChunk); processChunk = ""; errChunk = ""; t0 = System.currentTimeMillis(); @@ -1040,9 +1075,9 @@ public void runMamba(Consumer consumer, final String... args ) throws Ru errBuff.append(new String(buffer, 0, errStream.read(buffer))); errChunk += ERR_STREAM_UUUID + errBuff.toString().trim(); } - consumer.accept(errChunk); - consumer.accept(processChunk + System.lineSeparator() - + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED INSTALLATION"); + this.errConsumer.accept(errChunk); + this.consoleConsumer.accept(processChunk + System.lineSeparator() + + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED PROCESS"); } catch (IOException | InterruptedException e) { e.printStackTrace(); } From 4a0be2799c01b47cb60f17255f7904bf87bd871b Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 13:16:56 +0100 Subject: [PATCH 044/120] remove now unnecessary methods --- src/main/java/org/apposed/appose/Mamba.java | 40 +++++---------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index f2663ec..070fff8 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -541,27 +541,6 @@ public void createWithYaml( final String envName, final String envYaml ) throws createWithYaml(envName, envYaml, false); } - /** - * Run {@code conda create} to create a conda environment defined by the input environment yaml file. - * - * @param envName - * The environment name to be created. - * @param envYaml - * The environment yaml file containing the information required to build it - * @param consumer - * String consumer that keeps track of the environment creation - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - public void createWithYaml( final String envName, final String envYaml, Consumer consumer ) throws IOException, InterruptedException - { - createWithYaml(envName, envYaml, false, consumer); - } - /** * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * @@ -603,8 +582,6 @@ public void createWithYaml( final String envName, final String envYaml, final bo * Force creation of the environment if {@code true}. If this value * is {@code false} and an environment with the specified name * already exists, throw an {@link EnvironmentExistsException}. - * @param consumer - * String consumer that keeps track of the environment creation * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -612,11 +589,11 @@ public void createWithYaml( final String envName, final String envYaml, final bo * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation, Consumer consumer) throws IOException, InterruptedException + public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runMamba(consumer, "env", "create", "--prefix", + runMamba("env", "create", "--prefix", envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); } @@ -1008,6 +985,9 @@ public String getVersion() throws IOException, InterruptedException * String consumer that receives the Strings that the process prints to the console * @param args * One or more arguments for the Conda command. + * @param isInheritIO + * Sets the source and destination for subprocess standard I/O to be + * the same as those of the current Java process. * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -1015,7 +995,7 @@ public String getVersion() throws IOException, InterruptedException * is waiting, then the wait is ended and an InterruptedException is * thrown. */ - public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException { Thread mainThread = Thread.currentThread(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); @@ -1024,7 +1004,7 @@ public void runMamba(final String... args ) throws RuntimeException, IOException cmd.add( mambaCommand ); cmd.addAll( Arrays.asList( args ) ); - ProcessBuilder builder = getBuilder(false).command(cmd); + ProcessBuilder builder = getBuilder(isInheritIO).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. this.consoleConsumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); @@ -1105,11 +1085,7 @@ public void runMamba(final String... args ) throws RuntimeException, IOException */ public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException { - final List< String > cmd = getBaseCommand(); - cmd.add( mambaCommand ); - cmd.addAll( Arrays.asList( args ) ); - if ( getBuilder( true ).command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException(); + runMamba(false, args); } /** From c48b3d509cf65f2ea94ea690d41cd593191939dc Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 14:20:06 +0100 Subject: [PATCH 045/120] add logic to track progress of download --- .../org/apposed/appose/FileDownloader.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/main/java/org/apposed/appose/FileDownloader.java diff --git a/src/main/java/org/apposed/appose/FileDownloader.java b/src/main/java/org/apposed/appose/FileDownloader.java new file mode 100644 index 0000000..b659d8c --- /dev/null +++ b/src/main/java/org/apposed/appose/FileDownloader.java @@ -0,0 +1,55 @@ +package org.apposed.appose; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; + +public class FileDownloader { + private ReadableByteChannel rbc; + private FileOutputStream fos; + private static final long CHUNK_SIZE = 1024 * 1024 * 5; + + public FileDownloader(ReadableByteChannel rbc, FileOutputStream fos) { + this.rbc = rbc; + this.fos = fos; + } + + /** + * Download a file without the possibility of interrupting the download + * @throws IOException if there is any error downloading the file from the url + */ + public void call() throws IOException { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + + /** + * Download a file with the possibility of interrupting the download if the parentThread is + * interrupted + * + * @param parentThread + * thread from where the download was launched, it is the reference used to stop the download + * @throws IOException if there is any error downloading the file from the url + * @throws InterruptedException if the download is interrupted because the parentThread is interrupted + */ + public void call(Thread parentThread) throws IOException, InterruptedException { + long position = 0; + while (true) { + long transferred = fos.getChannel().transferFrom(rbc, position, CHUNK_SIZE); + if (transferred == 0) { + break; + } + + position += transferred; + if (!parentThread.isAlive()) { + // Close resources if needed and exit + closeResources(); + throw new InterruptedException("File download was interrupted."); + } + } + } + + private void closeResources() throws IOException { + if (rbc != null) rbc.close(); + if (fos != null) fos.close(); + } +} From ed00b6899dbc76fc5afa985e3d135b4d12628f13 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 14:23:53 +0100 Subject: [PATCH 046/120] reduce method size and improve micromamba download --- src/main/java/org/apposed/appose/Mamba.java | 73 +++++++++---------- .../apposed/appose/MambaInstallerUtils.java | 30 +++++++- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 070fff8..540327f 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -32,6 +32,7 @@ import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -101,11 +102,15 @@ public class Mamba { * Consumer that tracks the progress in the download of micromamba, the software used * by this class to manage Python environments */ - private Consumer mambaDnwldProgressConsumer; + private Consumer mambaDnwldProgressConsumer = (p) -> { + mambaDnwldProgress = p; + }; /** * Consumer that tracks the progress decompressing the downloaded micromamba files. */ - private Consumer mambaDecompressProgressConsumer; + private Consumer mambaDecompressProgressConsumer = (p) -> { + mambaDecompressProgress = p; + }; /** * String that contains all the console output produced by micromamba ever since the {@link Mamba} was instantiated */ @@ -432,15 +437,31 @@ public void setErrorOutputConsumer(Consumer custom) { this.customErrorConsumer = custom; } - private void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { - + private File downloadMicromamba() throws IOException, URISyntaxException { final File tempFile = File.createTempFile( "micromamba", ".tar.bz2" ); tempFile.deleteOnExit(); URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); - ReadableByteChannel rbc = Channels.newChannel(website.openStream()); - try (FileOutputStream fos = new FileOutputStream(tempFile)) { - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } + long size = MambaInstallerUtils.getFileSize(website); + Thread currentThread = Thread.currentThread(); + Thread dwnldThread = new Thread(() -> { + try ( + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(tempFile); + ) { + new FileDownloader(rbc, fos).call(currentThread); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + }); + dwnldThread.start(); + while (dwnldThread.isAlive()) + this.mambaDnwldProgressConsumer.accept(((double) tempFile.length()) / ((double) size)); + if ((((double) tempFile.length()) / ((double) size)) < 1) + throw new IOException("Error downloading micromamba from: " + MICROMAMBA_URL); + return tempFile; + } + + private void decompressMicromamba(final File tempFile) throws FileNotFoundException, IOException, ArchiveException { final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); @@ -450,16 +471,16 @@ private void installMicromamba() throws IOException, InterruptedException, Archi MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) throw new IOException("Failed to create Micromamba default envs directory " + envsdir); - - - // The following command will throw an exception if Conda does not work as - // expected. boolean executableSet = new File(mambaCommand).setExecutable(true); if (!executableSet) throw new IOException("Cannot set file as executable due to missing permissions, " + "please do it manually: " + mambaCommand); } + private void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { + decompressMicromamba(downloadMicromamba()); + } + public String getEnvsDir() { return this.envsdir; } @@ -541,34 +562,6 @@ public void createWithYaml( final String envName, final String envYaml ) throws createWithYaml(envName, envYaml, false); } - /** - * Run {@code conda create} to create a conda environment defined by the input environment yaml file. - * - * @param envName - * The environment name to be created. - * @param envYaml - * The environment yaml file containing the information required to build it - * @param envName - * The environment name to be created. - * @param isForceCreation - * Force creation of the environment if {@code true}. If this value - * is {@code false} and an environment with the specified name - * already exists, throw an {@link EnvironmentExistsException}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation ) throws IOException, InterruptedException - { - if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) - throw new EnvironmentExistsException(); - runMamba( "env", "create", "--prefix", - envsdir + File.separator + envName, "-f", envYaml, "-y" ); - } - /** * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java index c4e520b..97daf0f 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -53,7 +53,7 @@ import org.apache.commons.compress.utils.IOUtils; /** - * Utility methods unzip bzip2 files + * Utility methods unzip bzip2 files and to enable the download of micromamba */ public final class MambaInstallerUtils { @@ -186,4 +186,32 @@ public static URL redirectedURL(URL url) throws MalformedURLException, URISyntax String mainDomain = scheme + "://" + host; return redirectedURL(new URL(mainDomain + newURL)); } + + /** + * Get the size of the file stored in the given URL + * @param url + * url where the file is stored + * @return the size of the file + */ + public static long getFileSize(URL url) { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "Appose/0.1.0(" + System.getProperty("os.name") + "; Java " + System.getProperty("java.version")); + if (conn.getResponseCode() >= 300 && conn.getResponseCode() <= 308) + return getFileSize(redirectedURL(url)); + if (conn.getResponseCode() != 200) + throw new Exception("Unable to connect to: " + url.toString()); + long size = conn.getContentLengthLong(); + conn.disconnect(); + return size; + } catch (IOException e) { + throw new RuntimeException(e); + } catch (Exception ex) { + ex.printStackTrace(); + String msg = "Unable to connect to " + url.toString(); + System.out.println(msg); + return 1; + } + } } From 8e5aa0402f69ad4cf04412f7fdf771320e73d338 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 18:01:57 +0100 Subject: [PATCH 047/120] add forgotten exception --- src/main/java/org/apposed/appose/Builder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 9562583..d661d93 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -56,7 +56,7 @@ public Environment build() { // TODO: Should we update it? For now, we just use it. } else { - conda.create(envName, "-f", condaEnvironmentYaml.getAbsolutePath()); + conda.createWithYaml(envName, condaEnvironmentYaml.getAbsolutePath()); } } catch (IOException e) { throw new RuntimeException(e); @@ -66,6 +66,8 @@ public Environment build() { throw new RuntimeException(e); } catch (URISyntaxException e) { throw new RuntimeException(e); + } catch (MambaInstallException e) { + throw new RuntimeException(e); } return new Environment() { From 562a2a7aab537d34a3aa3efb506b332227fab6fd Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 18:06:17 +0100 Subject: [PATCH 048/120] add -c to run code --- src/main/java/org/apposed/appose/Mamba.java | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 540327f..f9d8ba5 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1349,16 +1349,13 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String min * @return true if the package is installed or false otherwise */ public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { - File envFile = new File(this.envsdir, envName); - File envFile2 = new File(envName); - if (!envFile.isDirectory() && !envFile2.isDirectory()) + File envFile = new File(envDir); + if (!envFile.isDirectory()) return false; - else if (!envFile.isDirectory()) - envFile = envFile2; if (dependency.trim().equals("python")) return checkPythonInstallation(envDir, minversion, maxversion, strictlyBiggerOrSmaller); String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { - checkDepCode = "import importlib, sys; " + checkDepCode = "import importlib.util, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = %s; wanted_v = %s; " @@ -1366,10 +1363,10 @@ else if (!envFile.isDirectory()) + "sys.exit(0) if spec and vv.parse(version(pkg)) == vv.parse(wanted_v) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, maxversion); } else if (minversion == null && maxversion == null) { - checkDepCode = "import importlib, sys; sys.exit(0) if importlib.util.find_spec(%s) else sys.exit(1)"; + checkDepCode = "import importlib.util, sys; sys.exit(0) if importlib.util.find_spec('%s') else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency); } else if (maxversion == null) { - checkDepCode = "import importlib, sys; " + checkDepCode = "import importlib.util, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " @@ -1377,7 +1374,7 @@ else if (!envFile.isDirectory()) + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, minversion, strictlyBiggerOrSmaller ? ">" : ">="); } else if (minversion == null) { - checkDepCode = "import importlib, sys; " + checkDepCode = "import importlib.util, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = '%s'; desired_version = '%s'; " @@ -1385,7 +1382,7 @@ else if (!envFile.isDirectory()) + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); } else { - checkDepCode = "import importlib, sys; " + checkDepCode = "import importlib.util, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " + "pkg = '%s'; min_v = '%s'; max_v = '%s'; " @@ -1394,7 +1391,7 @@ else if (!envFile.isDirectory()) checkDepCode = String.format(checkDepCode, dependency, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); } try { - runPythonIn(envFile, checkDepCode); + runPythonIn(envFile, "-c", checkDepCode); } catch (RuntimeException | IOException | InterruptedException e) { return false; } @@ -1425,7 +1422,7 @@ private boolean checkPythonInstallation(String envDir, String minversion, String checkDepCode = String.format(checkDepCode, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); } try { - runPythonIn(envFile, checkDepCode); + runPythonIn(envFile, "-c", checkDepCode); } catch (RuntimeException | IOException | InterruptedException e) { return false; } From 586a5e166e8a29d13edad61d80e2ae29531ce6ff Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 18:10:10 +0100 Subject: [PATCH 049/120] allow providing env name or env dir --- src/main/java/org/apposed/appose/Mamba.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index f9d8ba5..d55f572 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1349,9 +1349,12 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String min * @return true if the package is installed or false otherwise */ public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { - File envFile = new File(envDir); - if (!envFile.isDirectory()) + File envFile = new File(this.envsdir, envDir); + File envFile2 = new File(envDir); + if (!envFile.isDirectory() && !envFile2.isDirectory()) return false; + else if (!envFile.isDirectory()) + envFile = envFile2; if (dependency.trim().equals("python")) return checkPythonInstallation(envDir, minversion, maxversion, strictlyBiggerOrSmaller); String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { @@ -1399,7 +1402,12 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String min } private boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { - File envFile = new File(envDir); + File envFile = new File(this.envsdir, envDir); + File envFile2 = new File(envDir); + if (!envFile.isDirectory() && !envFile2.isDirectory()) + return false; + else if (!envFile.isDirectory()) + envFile = envFile2; String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " From bd9137900e5a2ac5cb65760e54a1173e5c130694 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 18:12:28 +0100 Subject: [PATCH 050/120] correct error in logic --- src/main/java/org/apposed/appose/Mamba.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index d55f572..6629224 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1197,10 +1197,10 @@ public List checkUninstalledDependenciesInEnv(String envName, List uninstalled = dependencies.stream().filter(dep -> { int ind = dep.indexOf("="); - if (ind == -1) return checkDependencyInEnv(envName, dep); + if (ind == -1) return !checkDependencyInEnv(envName, dep); String packName = dep.substring(0, ind); String vv = dep.substring(ind + 1); - return checkDependencyInEnv(envName, packName, vv); + return !checkDependencyInEnv(envName, packName, vv); }).collect(Collectors.toList()); return uninstalled; } From 4e7010becf228d23c9098c937d2c7791590ae445 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 19:01:36 +0100 Subject: [PATCH 051/120] cathc every exception --- src/main/java/org/apposed/appose/Mamba.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 6629224..220d6dd 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -375,7 +375,7 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException boolean installed = true; try { getVersion(); - } catch (RuntimeException ex) { + } catch (Exception ex) { installed = false; } if (!installed && !installIfMissing) From 686e8f82d3a665c41c56de7b400ca44842f94f3c Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 19:12:52 +0100 Subject: [PATCH 052/120] recover methods that i thought were not useful --- src/main/java/org/apposed/appose/Mamba.java | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 220d6dd..1eace75 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -630,6 +630,56 @@ public void create( final String envName, final boolean isForceCreation ) throws runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); } + /** + * Run {@code conda create} to create a new mamba environment with a list of + * specified packages. + * + * @param envName + * The environment name to be created. + * @param args + * The list of packages to be installed on environment creation and + * extra parameters as {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName, final String... args ) throws IOException, InterruptedException + { + create( envName, false, args ); + } + + /** + * Run {@code conda create} to create a new conda environment with a list of + * specified packages. + * + * @param envName + * The environment name to be created. + * @param isForceCreation + * Force creation of the environment if {@code true}. If this value + * is {@code false} and an environment with the specified name + * already exists, throw an {@link EnvironmentExistsException}. + * @param args + * The list of packages to be installed on environment creation and + * extra parameters as {@code String...}. + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + */ + public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException + { + if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) + throw new EnvironmentExistsException(); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); + cmd.addAll( Arrays.asList( args ) ); + runMamba( cmd.stream().toArray( String[]::new ) ); + } + /** * Run {@code conda create} to create a new conda environment with a list of * specified packages. From 8977e29d530be8ea3cb47a66ecdf243d98a4a1c2 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 19:21:52 +0100 Subject: [PATCH 053/120] remove force argument --- src/main/java/org/apposed/appose/Mamba.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 1eace75..b55dc9c 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -675,7 +675,7 @@ public void create( final String envName, final boolean isForceCreation, final S { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "-p", envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -707,7 +707,7 @@ public void create( final String envName, final boolean isForceCreation, List cmd = new ArrayList<>( Arrays.asList( "env", "create", "--force", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "-p", envsdir + File.separator + envName ) ); if (channels == null) channels = new ArrayList(); for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} if (packages == null) packages = new ArrayList(); From ddc6c3a7b0fc12f10c4df6a6c1872eb8ff5279fc Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Tue, 30 Jan 2024 20:26:13 +0100 Subject: [PATCH 054/120] add automatic yes for everything --- src/main/java/org/apposed/appose/Mamba.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index b55dc9c..0709258 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -538,8 +538,9 @@ public void update( final String... args ) throws IOException, InterruptedExcept */ public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException { - final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-y", "-p", this.envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); + if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -675,8 +676,9 @@ public void create( final String envName, final boolean isForceCreation, final S { if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "env", "create", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); + if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -707,11 +709,12 @@ public void create( final String envName, final boolean isForceCreation, List cmd = new ArrayList<>( Arrays.asList( "env", "create", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) ); if (channels == null) channels = new ArrayList(); for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} if (packages == null) packages = new ArrayList(); for (String pack : packages) { cmd.add(pack);} + cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -852,8 +855,9 @@ public void installIn( final String envName, List channels, List */ public void installIn( final String envName, final String... args ) throws IOException, InterruptedException { - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); + if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); } From bc7bcbcc07e89151d30e687e37786c1dbbd7b037 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Thu, 1 Feb 2024 19:26:26 +0100 Subject: [PATCH 055/120] do not do the installation at the same time as the instantiation --- src/main/java/org/apposed/appose/Mamba.java | 314 ++++++++++++-------- 1 file changed, 185 insertions(+), 129 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 0709258..f70e39a 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -88,6 +88,10 @@ public class Mamba { * Path to the folder that contains the directories */ private final String envsdir; + /** + * Whether Micromamba is installed or not + */ + private boolean installed = false; /** * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. * @@ -245,84 +249,7 @@ public Mamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException, MambaInstallException { - this(BASE_PATH, false); - } - - /** - * Create a new {@link Mamba} object. The root dir for the Micromamba installation - * will be the default base path defined at {@link BASE_PATH} - * If there is no Micromamba found at the specified - * path, it will be installed automatically if the parameter 'installIfNeeded' - * is true. If not a {@link MambaInstallException} will be thrown. - * - * It is expected that the Micromamba installation has executable commands as shown below: - * - *
-	 * MAMBA_ROOT
-	 * ├── bin
-	 * │   ├── micromamba(.exe)
-	 * │   ... 
-	 * ├── envs
-	 * │   ├── your_env
-	 * │   │   ├── python(.exe)
-	 * 
- * - * @param installIfNeeded - * if Micormamba is not installed in the default dir {@link #BASE_PATH}, Appose installs it - * automatically - * - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws ArchiveException - * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been installed in the default path {@link #BASE_PATH} - * and 'installIfNeeded' is false - */ - public Mamba(boolean installIfNeeded) throws IOException, - InterruptedException, ArchiveException, - URISyntaxException, MambaInstallException - { - this(BASE_PATH, installIfNeeded); - } - - /** - * Create a new Mamba object. The root dir for Mamba installation can be - * specified as {@code String}. - * If there is no Micromamba found at the specified path, a {@link MambaInstallException} will be thrown. - * - * It is expected that the Micromamba installation has executable commands as shown below: - * - *
-	 * MAMBA_ROOT
-	 * ├── bin
-	 * │   ├── micromamba(.exe)
-	 * │   ... 
-	 * ├── envs
-	 * │   ├── your_env
-	 * │   │   ├── python(.exe)
-	 * 
- * - * @param rootdir - * The root dir for Mamba installation. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws ArchiveException - * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been installed in the provided 'rootdir' path - */ - public Mamba( final String rootdir) throws IOException, - InterruptedException, ArchiveException, - URISyntaxException, MambaInstallException - { - this(rootdir, false); + this(BASE_PATH); } /** @@ -345,9 +272,6 @@ public Mamba( final String rootdir) throws IOException, * * @param rootdir * The root dir for Mamba installation. - * @param installIfNeeded - * if Micormamba is not installed in the path specified by 'rootdir', Appose installs it - * automatically * @throws IOException * If an I/O error occurs. * @throws InterruptedException @@ -359,7 +283,7 @@ public Mamba( final String rootdir) throws IOException, * @throws MambaInstallException if Micromamba has not been installed in the dir defined by 'rootdir' * and 'installIfNeeded' is false */ - public Mamba( final String rootdir, boolean installIfMissing) throws IOException, + public Mamba( final String rootdir) throws IOException, InterruptedException, ArchiveException, URISyntaxException, MambaInstallException { @@ -370,20 +294,29 @@ public Mamba( final String rootdir, boolean installIfMissing) throws IOException this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH; this.envsdir = this.rootdir + File.separator + ENVS_NAME; boolean filesExist = Files.notExists( Paths.get( mambaCommand ) ); - if (!filesExist && !installIfMissing) - throw new MambaInstallException(); - boolean installed = true; + if (!filesExist) + return; try { getVersion(); } catch (Exception ex) { - installed = false; - } - if (!installed && !installIfMissing) - throw new MambaInstallException("Even though Micromamba installation has been found, " - + "it did not work as expected. Re-installation is advised. Path of installation found: " + this.rootdir); - if (installed) return; - installMicromamba(); + } + installed = true; + } + + /** + * Check whether micromamba is installed or not to be able to use the instance of {@link Mamba} + * @return whether micromamba is installed or not to be able to use the instance of {@link Mamba} + */ + public boolean checkMambaInstalled() { + try { + getVersion(); + this.installed = true; + } catch (Exception ex) { + this.installed = false; + return false; + } + return true; } /** @@ -477,7 +410,22 @@ private void decompressMicromamba(final File tempFile) throws FileNotFoundExcept + "please do it manually: " + mambaCommand); } - private void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { + /** + * Install Micromamba automatically + * @throws IOException + * If an I/O error occurs. + * @throws InterruptedException + * If the current thread is interrupted by another thread while it + * is waiting, then the wait is ended and an InterruptedException is + * thrown. + * @throws ArchiveException + * @throws URISyntaxException + * @throws MambaInstallException if Micromamba has not been installed in the dir defined by 'rootdir' + * and 'installIfNeeded' is false + */ + public void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { + checkMambaInstalled(); + if (installed) return; decompressMicromamba(downloadMicromamba()); } @@ -514,9 +462,12 @@ private static List< String > getBaseCommand() * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void update( final String... args ) throws IOException, InterruptedException + public void update( final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); updateIn( envName, args ); } @@ -535,9 +486,12 @@ public void update( final String... args ) throws IOException, InterruptedExcept * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException + public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); @@ -557,9 +511,12 @@ public void updateIn( final String envName, final String... args ) throws IOExce * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void createWithYaml( final String envName, final String envYaml ) throws IOException, InterruptedException + public void createWithYaml( final String envName, final String envYaml ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); createWithYaml(envName, envYaml, false); } @@ -582,9 +539,14 @@ public void createWithYaml( final String envName, final String envYaml ) throws * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws RuntimeException + * If there is any error running the commands + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException + public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException, RuntimeException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", @@ -602,9 +564,12 @@ public void createWithYaml( final String envName, final String envYaml, final bo * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName ) throws IOException, InterruptedException + public void create( final String envName ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); create( envName, false ); } @@ -623,9 +588,14 @@ public void create( final String envName ) throws IOException, InterruptedExcept * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws RuntimeException + * If there is any error running the commands + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName, final boolean isForceCreation ) throws IOException, InterruptedException + public void create( final String envName, final boolean isForceCreation ) throws IOException, InterruptedException, RuntimeException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); @@ -646,9 +616,12 @@ public void create( final String envName, final boolean isForceCreation ) throws * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName, final String... args ) throws IOException, InterruptedException + public void create( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); create( envName, false, args ); } @@ -671,9 +644,12 @@ public void create( final String envName, final String... args ) throws IOExcept * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException + public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) ); @@ -703,9 +679,14 @@ public void create( final String envName, final boolean isForceCreation, final S * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws RuntimeException + * If there is any error running the commands + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException + public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); @@ -714,7 +695,7 @@ public void create( final String envName, final boolean isForceCreation, List(); for (String pack : packages) { cmd.add(pack);} - cmd.add("--yes"); + if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); } @@ -726,9 +707,12 @@ public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException + public void install( List channels, List packages ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); installIn( envName, channels, packages ); } @@ -825,9 +818,13 @@ public void install( List channels, List packages ) throws IOExc * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws RuntimeException + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void installIn( final String envName, List channels, List packages ) throws IOException, InterruptedException + public void installIn( final String envName, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) ); if (channels == null) channels = new ArrayList(); @@ -852,9 +849,12 @@ public void installIn( final String envName, List channels, List * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void installIn( final String envName, final String... args ) throws IOException, InterruptedException + public void installIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", this.envsdir + File.separator + envName ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); @@ -874,9 +874,12 @@ public void installIn( final String envName, final String... args ) throws IOExc * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void pipInstall( final String... args ) throws IOException, InterruptedException + public void pipInstall( final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); pipInstallIn( envName, args ); } @@ -895,9 +898,12 @@ public void pipInstall( final String... args ) throws IOException, InterruptedEx * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void pipInstallIn( final String envName, final String... args ) throws IOException, InterruptedException + public void pipInstallIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = new ArrayList<>( Arrays.asList( "-m", "pip", "install" ) ); cmd.addAll( Arrays.asList( args ) ); runPythonIn( envName, cmd.stream().toArray( String[]::new ) ); @@ -917,9 +923,12 @@ public void pipInstallIn( final String envName, final String... args ) throws IO * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runPython( final String... args ) throws IOException, InterruptedException + public void runPython( final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); runPythonIn( envName, args ); } @@ -939,9 +948,12 @@ public void runPython( final String... args ) throws IOException, InterruptedExc * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runPythonIn( final String envName, final String... args ) throws IOException, InterruptedException + public void runPythonIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); if ( envName.equals( DEFAULT_ENVIRONMENT_NAME ) ) cmd.add( PYTHON_COMMAND ); @@ -1014,9 +1026,12 @@ public static void runPythonIn( final File envFile, final String... args ) throw * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public String getVersion() throws IOException, InterruptedException + public String getVersion() throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); cmd.addAll( Arrays.asList( mambaCommand, "--version" ) ); final Process process = getBuilder( false ).command( cmd ).start(); @@ -1035,15 +1050,20 @@ public String getVersion() throws IOException, InterruptedException * @param isInheritIO * Sets the source and destination for subprocess standard I/O to be * the same as those of the current Java process. + * @throws RuntimeException + * If there is any error running the commands * @throws IOException * If an I/O error occurs. * @throws InterruptedException * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); Thread mainThread = Thread.currentThread(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); @@ -1123,15 +1143,20 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE * * @param args * One or more arguments for the Conda command. + * @throws RuntimeException + * If there is any error running the commands * @throws IOException * If an I/O error occurs. * @throws InterruptedException * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException + public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); runMamba(false, args); } @@ -1202,9 +1227,12 @@ public Map< String, String > getEnvironmentVariables( final String envName ) thr * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public List< String > getEnvironmentNames() throws IOException + public List< String > getEnvironmentNames() throws IOException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > envs = new ArrayList<>( Arrays.asList( DEFAULT_ENVIRONMENT_NAME ) ); envs.addAll( Files.list( Paths.get( envsdir ) ) .map( p -> p.getFileName().toString() ) @@ -1225,8 +1253,11 @@ public List< String > getEnvironmentNames() throws IOException * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the packages are installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkAllDependenciesInEnv(String envName, List dependencies) { + public boolean checkAllDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); return checkUninstalledDependenciesInEnv(envName, dependencies).size() == 0; } @@ -1243,18 +1274,25 @@ public boolean checkAllDependenciesInEnv(String envName, List dependenci * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the packages are installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public List checkUninstalledDependenciesInEnv(String envName, List dependencies) { + public List checkUninstalledDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); File envFile = new File(this.envsdir, envName); File envFile2 = new File(envName); if (!envFile.isDirectory() && !envFile2.isDirectory()) return dependencies; List uninstalled = dependencies.stream().filter(dep -> { - int ind = dep.indexOf("="); - if (ind == -1) return !checkDependencyInEnv(envName, dep); - String packName = dep.substring(0, ind); - String vv = dep.substring(ind + 1); + try { + int ind = dep.indexOf("="); + if (ind == -1) return !checkDependencyInEnv(envName, dep); + String packName = dep.substring(0, ind); + String vv = dep.substring(ind + 1); return !checkDependencyInEnv(envName, packName, vv); + } catch (Exception ex) { + return true; + } }).collect(Collectors.toList()); return uninstalled; } @@ -1271,8 +1309,11 @@ public List checkUninstalledDependenciesInEnv(String envName, List=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the package is installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkDependencyInEnv(String envName, String dependency) { + public boolean checkDependencyInEnv(String envName, String dependency) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); if (dependency.contains("==")) { int ind = dependency.indexOf("=="); return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim()); @@ -1342,8 +1383,11 @@ public boolean checkDependencyInEnv(String envName, String dependency) { * @param version * the specific version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0" * @return true if the package is installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkDependencyInEnv(String envDir, String dependency, String version) { + public boolean checkDependencyInEnv(String envDir, String dependency, String version) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); return checkDependencyInEnv(envDir, dependency, version, version, true); } @@ -1373,8 +1417,11 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String ver * pacakge_version<1.9. * If there is no maximum version requirement for the package of interest, set this argument to null. * @return true if the package is installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) { + public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); return checkDependencyInEnv(envDir, dependency, minversion, maxversion, true); } @@ -1401,8 +1448,12 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String min * @param strictlyBiggerOrSmaller * Whether the minversion and maxversion shuld be strictly smaller and bigger or not * @return true if the package is installed or false otherwise + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { + public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, + String maxversion, boolean strictlyBiggerOrSmaller) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); File envFile = new File(this.envsdir, envDir); File envFile2 = new File(envDir); if (!envFile.isDirectory() && !envFile2.isDirectory()) @@ -1455,7 +1506,9 @@ else if (!envFile.isDirectory()) return true; } - private boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) { + private boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); File envFile = new File(this.envsdir, envDir); File envFile2 = new File(envDir); if (!envFile.isDirectory() && !envFile2.isDirectory()) @@ -1495,8 +1548,11 @@ else if (!envFile.isDirectory()) * TODO figure out whether to use a dependency or not to parse the yaml file * @param envYaml * @return + * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public boolean checkEnvFromYamlExists(String envYaml) { + public boolean checkEnvFromYamlExists(String envYaml) throws MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); if (envYaml == null || new File(envYaml).isFile() == false || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { return false; From 920d09dd391f6795e5721e5bdffbeb33de6f75e6 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 5 Feb 2024 13:28:40 +0100 Subject: [PATCH 056/120] check the installation works --- src/main/java/org/apposed/appose/Mamba.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index f70e39a..115f334 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -427,6 +427,7 @@ public void installMicromamba() throws IOException, InterruptedException, Archiv checkMambaInstalled(); if (installed) return; decompressMicromamba(downloadMicromamba()); + checkMambaInstalled(); } public String getEnvsDir() { From cb056b178064028750b7e61ecfc1d5e2e4ad8913 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 5 Feb 2024 13:56:36 +0100 Subject: [PATCH 057/120] do not throw exceptions when instantiating mamba --- src/main/java/org/apposed/appose/Mamba.java | 29 ++------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 115f334..71e43b6 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -235,20 +235,8 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) * │   │   ├── python(.exe) *
* - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws ArchiveException - * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been installed in the default path {@link #BASE_PATH} */ - public Mamba() throws IOException, - InterruptedException, ArchiveException, - URISyntaxException, MambaInstallException - { + public Mamba() { this(BASE_PATH); } @@ -272,21 +260,8 @@ public Mamba() throws IOException, * * @param rootdir * The root dir for Mamba installation. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws ArchiveException - * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been installed in the dir defined by 'rootdir' - * and 'installIfNeeded' is false */ - public Mamba( final String rootdir) throws IOException, - InterruptedException, ArchiveException, - URISyntaxException, MambaInstallException - { + public Mamba( final String rootdir) { if (rootdir == null) this.rootdir = BASE_PATH; else From b83da2ca41ba43613a43cf7e99161b21c5b09d49 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 5 Feb 2024 13:58:56 +0100 Subject: [PATCH 058/120] add method to install if it is missing --- src/main/java/org/apposed/appose/Builder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index d661d93..e275ecb 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -51,6 +51,7 @@ public Environment build() { try { Mamba conda = new Mamba(Mamba.BASE_PATH); + conda.installMicromamba(); String envName = "appose"; if (conda.getEnvironmentNames().contains( envName )) { // TODO: Should we update it? For now, we just use it. From 28e61ed22c8bfb2c48670c341ced4b25edf7e612 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 5 Feb 2024 14:02:33 +0100 Subject: [PATCH 059/120] incorrect logic --- src/main/java/org/apposed/appose/Mamba.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 71e43b6..71c5110 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1006,7 +1006,6 @@ public static void runPythonIn( final File envFile, final String... args ) throw */ public String getVersion() throws IOException, InterruptedException, MambaInstallException { - checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); cmd.addAll( Arrays.asList( mambaCommand, "--version" ) ); From dd8cdb7fda05b2d06e5e306b47d482f2a02efa59 Mon Sep 17 00:00:00 2001 From: carlosuc3m <100329787@alumnos.uc3m.es> Date: Mon, 5 Feb 2024 14:19:05 +0100 Subject: [PATCH 060/120] remove blocking code --- src/main/java/org/apposed/appose/Mamba.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 71c5110..c15256d 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1006,7 +1006,6 @@ public static void runPythonIn( final File envFile, final String... args ) throw */ public String getVersion() throws IOException, InterruptedException, MambaInstallException { - if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); cmd.addAll( Arrays.asList( mambaCommand, "--version" ) ); final Process process = getBuilder( false ).command( cmd ).start(); From ed58aff4546f339dece0c3beb816b201c507ba40 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 13:52:00 -0500 Subject: [PATCH 061/120] MambaInstallerUtils: inline copy helper method So that it can be interrupted while running multi-threaded, I think... Co-authored-by: carlosuc3m <100329787@alumnos.uc3m.es> --- .../apposed/appose/MambaInstallerUtils.java | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java index 97daf0f..6abd453 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -50,7 +50,6 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; -import org.apache.commons.compress.utils.IOUtils; /** * Utility methods unzip bzip2 files and to enable the download of micromamba @@ -71,16 +70,43 @@ private MambaInstallerUtils() { * destination folder where the contents of the file are going to be decompressed * @throws FileNotFoundException if the .bzip2 file is not found or does not exist * @throws IOException if the source file already exists or there is any error with the decompression + * @throws InterruptedException if the therad where the decompression is happening is interrupted */ - public static void unBZip2(File source, File destination) throws FileNotFoundException, IOException { + public static void unBZip2(File source, File destination) throws FileNotFoundException, IOException, InterruptedException { try ( BZip2CompressorInputStream input = new BZip2CompressorInputStream(new BufferedInputStream(new FileInputStream(source))); FileOutputStream output = new FileOutputStream(destination); ) { - IOUtils.copy(input, output); + copy(input, output); } } - + + /** + * Copies the content of a InputStream into an OutputStream + * + * @param input + * the InputStream to copy + * @param output + * the target, may be null to simulate output to dev/null on Linux and NUL on Windows + * @return the number of bytes copied + * @throws IOException if an error occurs copying the streams + * @throws InterruptedException if the thread where this is happening is interrupted + */ + private static long copy(final InputStream input, final OutputStream output) throws IOException, InterruptedException { + int bufferSize = 4096; + final byte[] buffer = new byte[bufferSize]; + int n = 0; + long count = 0; + while (-1 != (n = input.read(buffer))) { + if (Thread.currentThread().isInterrupted()) throw new InterruptedException("Decompressing stopped."); + if (output != null) { + output.write(buffer, 0, n); + } + count += n; + } + return count; + } + /** Untar an input file into an output file. * The output file is created in the output folder, having the same name @@ -92,7 +118,7 @@ public static void unBZip2(File source, File destination) throws FileNotFoundExc * @throws FileNotFoundException * @throws ArchiveException */ - public static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException { + public static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException, InterruptedException { try ( InputStream is = new FileInputStream(inputFile); @@ -113,7 +139,7 @@ public static void unTar(final File inputFile, final File outputDir) throws File throw new IOException("Failed to create directory " + outputFile.getParentFile().getAbsolutePath()); } try (OutputStream outputFileStream = new FileOutputStream(outputFile)) { - IOUtils.copy(debInputStream, outputFileStream); + copy(debInputStream, outputFileStream); } } } @@ -121,7 +147,7 @@ public static void unTar(final File inputFile, final File outputDir) throws File } - public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException { + public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException, InterruptedException { String url = Mamba.MICROMAMBA_URL; final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); tempFile.deleteOnExit(); From 202b480360cc187ee38171b690983181145f1781 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 13:55:27 -0500 Subject: [PATCH 062/120] Improve javadoc blurbs Co-authored-by: carlosuc3m <100329787@alumnos.uc3m.es> --- .../apposed/appose/MambaInstallerUtils.java | 16 +++++++++++---- src/main/java/org/apposed/appose/Types.java | 20 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java index 6abd453..4bd5c71 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -147,6 +147,16 @@ public static void unTar(final File inputFile, final File outputDir) throws File } + /** + * Example main method + * @param args + * no args are required + * @throws FileNotFoundException if some file is not found + * @throws IOException if there is any error reading or writting + * @throws ArchiveException if there is any error decompressing + * @throws URISyntaxException if the url is wrong or there is no internet connection + * @throws InterruptedException if there is interrruption + */ public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException, InterruptedException { String url = Mamba.MICROMAMBA_URL; final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); @@ -177,11 +187,9 @@ public static void main(String[] args) throws FileNotFoundException, IOException * has been redirected * @param url * original url. Connecting to that url must give a 301, 302 or 303 response code - * @param conn - * connection to the url * @return the redirected url - * @throws MalformedURLException - * @throws URISyntaxException + * @throws MalformedURLException if the url does not fulfil the requirements for an url to be correct + * @throws URISyntaxException if the url is incorrect or there is no internet connection */ public static URL redirectedURL(URL url) throws MalformedURLException, URISyntaxException { int statusCode; diff --git a/src/main/java/org/apposed/appose/Types.java b/src/main/java/org/apposed/appose/Types.java index bcc7768..705107e 100644 --- a/src/main/java/org/apposed/appose/Types.java +++ b/src/main/java/org/apposed/appose/Types.java @@ -41,16 +41,34 @@ private Types() { // NB: Prevent instantiation of utility class. } + /** + * Converts a Map into a JSON string. + * @param data + * data that wants to be encoded + * @return string containing the info of the data map + */ public static String encode(Map data) { return JsonOutput.toJson(data); } + /** + * Converts a JSON string into a map. + * @param json + * json string + * @return a map of with the information of the json + */ @SuppressWarnings("unchecked") public static Map decode(String json) { return (Map) new JsonSlurper().parseText(json); } - /** Dumps the given exception, including stack trace, to a string. */ + /** + * Dumps the given exception, including stack trace, to a string. + * + * @param t + * the given exception {@link Throwable} + * @return the String containing the whole exception trace + */ public static String stackTrace(Throwable t) { StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); From cd5087a67234dcf1516b94e47428e2079c650ae6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 14:12:54 -0500 Subject: [PATCH 063/120] Integrate JDLL's improvements to the Mamba builder It is now effectively the same as this version: https://github.com/bioimage-io/JDLL/blob/31c01632cbb2bf48da10468926d7be2b6fb41b53/src/main/java/io/bioimage/modelrunner/apposed/appose/Mamba.java To see the original list of commits and diff of changes, see: https://github.com/bioimage-io/JDLL/compare/ffabb22f2befbbc3c9a31b47bebc6ad382546dbd..31c01632cbb2bf48da10468926d7be2b6fb41b53#diff-ce110e1b892cd710043acc0ed775e7134a51d0adb0aa290a2a04d1a19f910372 and click the "Load diff" button to view the "large" diff. Co-authored-by: carlosuc3m <100329787@alumnos.uc3m.es> --- src/main/java/org/apposed/appose/Mamba.java | 362 ++++++++++++++------ 1 file changed, 261 insertions(+), 101 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index c15256d..1a3ea74 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -28,6 +28,9 @@ import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.lang3.SystemUtils; + +import com.sun.jna.Platform; + import org.apposed.appose.CondaException.EnvironmentExistsException; import java.io.BufferedReader; @@ -37,6 +40,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.Channels; @@ -53,6 +57,8 @@ import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * Python environment manager, implemented by delegating to micromamba. @@ -96,25 +102,21 @@ public class Mamba { * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. * */ - private double mambaDnwldProgress = 0.0; + private Double mambaDnwldProgress = 0.0; /** * Progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba * software. VAlue between 0 and 1. */ - private double mambaDecompressProgress = 0.0; + private Double mambaDecompressProgress = 0.0; /** * Consumer that tracks the progress in the download of micromamba, the software used * by this class to manage Python environments */ - private Consumer mambaDnwldProgressConsumer = (p) -> { - mambaDnwldProgress = p; - }; + private Consumer mambaDnwldProgressConsumer = this::updateMambaDnwldProgress; /** * Consumer that tracks the progress decompressing the downloaded micromamba files. */ - private Consumer mambaDecompressProgressConsumer = (p) -> { - mambaDecompressProgress = p; - }; + private Consumer mambaDecompressProgressConsumer = this::updateMambaDecompressProgress; /** * String that contains all the console output produced by micromamba ever since the {@link Mamba} was instantiated */ @@ -135,20 +137,12 @@ public class Mamba { * Consumer that tracks the console output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer consoleConsumer = (str) -> { - mambaConsoleOut += str; - if (customConsoleConsumer != null) - customConsoleConsumer.accept(str); - }; + private Consumer consoleConsumer = this::updateConsoleConsumer; /** * Consumer that tracks the error output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer errConsumer = (str) -> { - mambaConsoleErr += str; - if (customErrorConsumer != null) - customErrorConsumer.accept(str); - }; + private Consumer errConsumer = this::updateErrorConsumer; /* * Path to Python executable from the environment directory */ @@ -160,7 +154,7 @@ public class Mamba { /** * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ - private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? + private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" : File.separator + "bin" + File.separator + "micromamba"; /** @@ -199,6 +193,30 @@ private static String microMambaPlatform() { default: return null; } } + + private void updateMambaDnwldProgress(Double pp) { + double progress = pp != null ? pp : 0.0; + mambaDnwldProgress = progress * 1.0; + } + + private void updateConsoleConsumer(String str) { + if (str == null) str = ""; + mambaConsoleOut += str; + if (customConsoleConsumer != null) + customConsoleConsumer.accept(str); + } + + private void updateErrorConsumer(String str) { + if (str == null) str = ""; + mambaConsoleErr += str; + if (customErrorConsumer != null) + customErrorConsumer.accept(str); + } + + private void updateMambaDecompressProgress(Double pp) { + double progress = pp != null ? pp : 0.0; + this.mambaDecompressProgress = progress * 1.0; + } /** * Returns a {@link ProcessBuilder} with the working directory specified in the @@ -220,8 +238,8 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) /** * Create a new {@link Mamba} object. The root dir for the Micromamba installation - * will be the default base path defined at {@link BASE_PATH} - * If there is no Micromamba found at the base path {@link BASE_PATH}, a {@link MambaInstallException} will be thrown + * will be the default base path defined at {@link #BASE_PATH} + * If there is no Micromamba found at the base path {@link #BASE_PATH}, a {@link MambaInstallException} will be thrown * * It is expected that the Micromamba installation has executable commands as shown below: * @@ -266,8 +284,8 @@ public Mamba( final String rootdir) { this.rootdir = BASE_PATH; else this.rootdir = rootdir; - this.mambaCommand = this.rootdir + MICROMAMBA_RELATIVE_PATH; - this.envsdir = this.rootdir + File.separator + ENVS_NAME; + this.mambaCommand = new File(this.rootdir + MICROMAMBA_RELATIVE_PATH).getAbsolutePath(); + this.envsdir = Paths.get(rootdir, ENVS_NAME).toAbsolutePath().toString(); boolean filesExist = Files.notExists( Paths.get( mambaCommand ) ); if (!filesExist) return; @@ -304,8 +322,8 @@ public double getMicromambaDownloadProgress(){ /** * - * @returnthe the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba - * software. VAlue between 0 and 1. + * @return the the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba + * software. VAlue between 0 and 1. */ public double getMicromambaDecompressProgress(){ return this.mambaDecompressProgress; @@ -369,13 +387,15 @@ private File downloadMicromamba() throws IOException, URISyntaxException { return tempFile; } - private void decompressMicromamba(final File tempFile) throws FileNotFoundException, IOException, ArchiveException { + private void decompressMicromamba(final File tempFile) + throws FileNotFoundException, IOException, ArchiveException, InterruptedException { final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); File mambaBaseDir = new File(rootdir); if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) - throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath()); + throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath() + + ". Please try installing it in another directory."); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) throw new IOException("Failed to create Micromamba default envs directory " + envsdir); @@ -393,10 +413,8 @@ private void decompressMicromamba(final File tempFile) throws FileNotFoundExcept * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws ArchiveException - * @throws URISyntaxException - * @throws MambaInstallException if Micromamba has not been installed in the dir defined by 'rootdir' - * and 'installIfNeeded' is false + * @throws ArchiveException if there is any error decompressing + * @throws URISyntaxException if there is any error with the micromamba url */ public void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { checkMambaInstalled(); @@ -500,7 +518,7 @@ public void createWithYaml( final String envName, final String envYaml ) throws * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * * @param envName - * The environment name to be created. + * The environment name to be created. It should not be a path, just the name. * @param envYaml * The environment yaml file containing the information required to build it * @param envName @@ -515,18 +533,22 @@ public void createWithYaml( final String envName, final String envYaml ) throws * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws RuntimeException - * If there is any error running the commands + * @throws RuntimeException if the process to create the env of the yaml file is not terminated correctly. If there is any error running the commands * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException, RuntimeException, MambaInstallException { + if (envName.contains(File.pathSeparator)) + throw new IllegalArgumentException("The environment name should not contain the file separator character: '" + + File.separator + "'"); checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); + if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) + installApposeFromSource(envsdir + File.separator + envName); } /** @@ -575,6 +597,8 @@ public void create( final String envName, final boolean isForceCreation ) throws if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); + if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) + installApposeFromSource(envsdir + File.separator + envName); } /** @@ -632,6 +656,8 @@ public void create( final String envName, final boolean isForceCreation, final S cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba( cmd.stream().toArray( String[]::new ) ); + if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) + installApposeFromSource(envsdir + File.separator + envName); } /** @@ -673,6 +699,8 @@ public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOExc * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws RuntimeException + * @throws RuntimeException if the process to create the env of the yaml file is not terminated correctly. If there is any error running the commands * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ public void installIn( final String envName, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException @@ -909,6 +937,12 @@ public void runPython( final String... args ) throws IOException, InterruptedExc } /** + * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example + * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example + * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example + * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example + * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example + * * Run a Python command in the specified environment. This method automatically * sets environment variables associated with the specified environment. In * Windows, this method also sets the {@code PATH} environment variable so that @@ -931,16 +965,30 @@ public void runPythonIn( final String envName, final String... args ) throws IOE checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); - if ( envName.equals( DEFAULT_ENVIRONMENT_NAME ) ) - cmd.add( PYTHON_COMMAND ); - else - cmd.add( Paths.get( ENVS_NAME, envName, PYTHON_COMMAND ).toString() ); - cmd.addAll( Arrays.asList( args ) ); + List argsList = new ArrayList(); + String envDir; + if (new File(envName, PYTHON_COMMAND).isFile()) { + argsList.add( coverArgWithDoubleQuotes(Paths.get( envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); + envDir = Paths.get( envName ).toAbsolutePath().toString(); + } else if (Paths.get( this.envsdir, envName, PYTHON_COMMAND ).toFile().isFile()) { + argsList.add( coverArgWithDoubleQuotes(Paths.get( this.envsdir, envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); + envDir = Paths.get( envsdir, envName ).toAbsolutePath().toString(); + } else + throw new IOException("The environment provided (" + + envName + ") does not exist or does not contain a Python executable (" + PYTHON_COMMAND + ")."); + argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); + else return aa; + }).collect(Collectors.toList()) ); + boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + + if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); + else cmd.add(surroundWithQuotes(argsList)); + final ProcessBuilder builder = getBuilder( true ); if ( SystemUtils.IS_OS_WINDOWS ) { final Map< String, String > envs = builder.environment(); - final String envDir = Paths.get( rootdir, ENVS_NAME, envName ).toString(); envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); @@ -948,7 +996,7 @@ public void runPythonIn( final String envName, final String... args ) throws IOE } // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); if ( builder.command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException(); + throw new RuntimeException("Error executing the following command: " + builder.command()); } /** @@ -957,8 +1005,8 @@ public void runPythonIn( final String envName, final String... args ) throws IOE * Windows, this method also sets the {@code PATH} environment variable so that * the specified environment runs as expected. * - * @param envName - * The environment name used to run the Python command. + * @param envFile + * file corresponding to the environment directory * @param args * One or more arguments for the Python command. * @throws IOException @@ -974,8 +1022,18 @@ public static void runPythonIn( final File envFile, final String... args ) throw throw new IOException("No Python found in the environment provided. The following " + "file does not exist: " + Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath()); final List< String > cmd = getBaseCommand(); - cmd.add( Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath().toString() ); - cmd.addAll( Arrays.asList( args ) ); + List argsList = new ArrayList(); + argsList.add( coverArgWithDoubleQuotes(Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath().toString()) ); + argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + if (Platform.isWindows() && aa.contains(" ")) return coverArgWithDoubleQuotes(aa); + else return aa; + }).collect(Collectors.toList()) ); + boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + + if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); + else cmd.add(surroundWithQuotes(argsList)); + + final ProcessBuilder builder = new ProcessBuilder().directory( envFile ); builder.inheritIO(); if ( SystemUtils.IS_OS_WINDOWS ) @@ -989,7 +1047,7 @@ public static void runPythonIn( final File envFile, final String... args ) throw } // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); if ( builder.command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException(); + throw new RuntimeException("Error executing the following command: " + builder.command()); } /** @@ -1007,23 +1065,24 @@ public static void runPythonIn( final File envFile, final String... args ) throw public String getVersion() throws IOException, InterruptedException, MambaInstallException { final List< String > cmd = getBaseCommand(); - cmd.addAll( Arrays.asList( mambaCommand, "--version" ) ); + if (mambaCommand.contains(" ") && SystemUtils.IS_OS_WINDOWS) + cmd.add( surroundWithQuotes(Arrays.asList( coverArgWithDoubleQuotes(mambaCommand), "--version" )) ); + else + cmd.addAll( Arrays.asList( coverArgWithDoubleQuotes(mambaCommand), "--version" ) ); final Process process = getBuilder( false ).command( cmd ).start(); if ( process.waitFor() != 0 ) - throw new RuntimeException(); + throw new RuntimeException("Error getting Micromamba version"); return new BufferedReader( new InputStreamReader( process.getInputStream() ) ).readLine(); } /** * Run a Conda command with one or more arguments. * - * @param consumer - * String consumer that receives the Strings that the process prints to the console - * @param args - * One or more arguments for the Conda command. * @param isInheritIO * Sets the source and destination for subprocess standard I/O to be * the same as those of the current Java process. + * @param args + * One or more arguments for the Mamba command. * @throws RuntimeException * If there is any error running the commands * @throws IOException @@ -1042,9 +1101,17 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); final List< String > cmd = getBaseCommand(); - cmd.add( mambaCommand ); - cmd.addAll( Arrays.asList( args ) ); - + List argsList = new ArrayList(); + argsList.add( coverArgWithDoubleQuotes(mambaCommand) ); + argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); + else return aa; + }).collect(Collectors.toList()) ); + boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + + if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); + else cmd.add(surroundWithQuotes(argsList)); + ProcessBuilder builder = getBuilder(isInheritIO).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. @@ -1063,7 +1130,10 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE int newLineIndex; long t0 = System.currentTimeMillis(); while (process.isAlive() || inputStream.available() > 0) { - if (mainThread.isInterrupted()) return; + if (!mainThread.isAlive()) { + process.destroyForcibly(); + return; + } if (inputStream.available() > 0) { processBuff.append(new String(buffer, 0, inputStream.read(buffer))); while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { @@ -1105,11 +1175,18 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE }); // Start reading threads outputThread.start(); - int processResult = process.waitFor(); + int processResult; + try { + processResult = process.waitFor(); + } catch (InterruptedException ex) { + throw new InterruptedException("Mamba process stopped. The command being executed was: " + cmd); + } // Wait for all output to be read outputThread.join(); if (processResult != 0) - throw new RuntimeException("Error executing the following command: " + builder.command()); + throw new RuntimeException("Error executing the following command: " + builder.command() + + System.lineSeparator() + this.mambaConsoleOut + + System.lineSeparator() + this.mambaConsoleErr); } /** @@ -1192,15 +1269,10 @@ public Map< String, String > getEnvironmentVariables( final String envName ) thr */ /** - * Returns a list of the Conda environment names as {@code List< String >}. + * Returns a list of the Mamba environment names as {@code List< String >}. * - * @return The list of the Conda environment names as {@code List< String >}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. + * @return The list of the Mamba environment names as {@code List< String >}. + * @throws IOException If an I/O error occurs. * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ public List< String > getEnvironmentNames() throws IOException, MambaInstallException @@ -1225,7 +1297,7 @@ public List< String > getEnvironmentNames() throws IOException, MambaInstallExce * The list of dependencies that should be installed in the environment. * They can contain version requirements. The names should be the ones used to import the package inside python, * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" - * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the packages are installed or false otherwise * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ @@ -1246,7 +1318,7 @@ public boolean checkAllDependenciesInEnv(String envName, List dependenci * The list of dependencies that should be installed in the environment. * They can contain version requirements. The names should be the ones used to import the package inside python, * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" - * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the packages are installed or false otherwise * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ @@ -1259,11 +1331,7 @@ public List checkUninstalledDependenciesInEnv(String envName, List uninstalled = dependencies.stream().filter(dep -> { try { - int ind = dep.indexOf("="); - if (ind == -1) return !checkDependencyInEnv(envName, dep); - String packName = dep.substring(0, ind); - String vv = dep.substring(ind + 1); - return !checkDependencyInEnv(envName, packName, vv); + return !checkDependencyInEnv(envName, dep); } catch (Exception ex) { return true; } @@ -1277,11 +1345,11 @@ public List checkUninstalledDependenciesInEnv(String envName, List=0.43.1", "torch==1.6", "torch>=1.6, <2.0" + * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" * @return true if the package is installed or false otherwise * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ @@ -1339,7 +1407,10 @@ public boolean checkDependencyInEnv(String envName, String dependency) throws Ma } else if (dependency.contains("<")) { int ind = dependency.indexOf("<"); return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim(), null, true); - } else { + } else if (dependency.contains("=")) { + int ind = dependency.indexOf("="); + return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim()); + }else { return checkDependencyInEnv(envName, dependency, null); } } @@ -1347,10 +1418,10 @@ public boolean checkDependencyInEnv(String envName, String dependency) throws Ma /** * Checks whether a package of a specific version is installed in the wanted environment. * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * @param envDir + * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. * This parameter can also be the full path to an independent environment. - * @param dependencies + * @param dependency * The name of the package that should be installed in the env. The String should only contain the name, no version, * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" * or "sklearn", not "scikit-learn". @@ -1369,26 +1440,26 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String ver * Checks whether a package with specific version constraints is installed in the wanted environment. * In this method the minversion argument should be strictly smaller than the version of interest and * the maxversion strictly bigger. - * This method checks that: dependency >minversion, =minversion, <=maxversion) look at the method + * This method checks that: dependency >minversion, <maxversion + * For smaller or equal or bigger or equal (dependency >=minversion, <=maxversion) look at the method * {@link #checkDependencyInEnv(String, String, String, String, boolean)} with the lst parameter set to false. * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * @param envDir + * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. * This parameter can also be the full path to an independent environment. - * @param dependencies + * @param dependency * The name of the package that should be installed in the env. The String should only contain the name, no version, * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" * or "sklearn", not "scikit-learn". * @param minversion * the minimum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". * This version should be strictly smaller than the one of interest, if for example "1.9" is given, it is assumed that - * pacakge_version>1.9. + * package_version>1.9. * If there is no minimum version requirement for the package of interest, set this argument to null. * @param maxversion * the maximum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". * This version should be strictly bigger than the one of interest, if for example "1.9" is given, it is assumed that - * pacakge_version<1.9. + * package_version<1.9. * If there is no maximum version requirement for the package of interest, set this argument to null. * @return true if the package is installed or false otherwise * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used @@ -1402,14 +1473,14 @@ public boolean checkDependencyInEnv(String envDir, String dependency, String min /** * Checks whether a package with specific version constraints is installed in the wanted environment. * Depending on the last argument ('strictlyBiggerOrSmaller') 'minversion' and 'maxversion' - * will be strictly bigger(>=) or smaller(<) or bigger or equal (>=) or smaller or equal<>=) + * will be strictly bigger(>=) or smaller(<) or bigger or equal >=) or smaller or equal<=) * In this method the minversion argument should be strictly smaller than the version of interest and * the maxversion strictly bigger. * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. + * @param envDir + * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. * This parameter can also be the full path to an independent environment. - * @param dependencies + * @param dependency * The name of the package that should be installed in the env. The String should only contain the name, no version, * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" * or "sklearn", not "scikit-learn". @@ -1440,7 +1511,7 @@ else if (!envFile.isDirectory()) checkDepCode = "import importlib.util, sys; " + "from importlib.metadata import version; " + "from packaging import version as vv; " - + "pkg = %s; wanted_v = %s; " + + "pkg = '%s'; wanted_v = '%s'; " + "spec = importlib.util.find_spec(pkg); " + "sys.exit(0) if spec and vv.parse(version(pkg)) == vv.parse(wanted_v) else sys.exit(1)"; checkDepCode = String.format(checkDepCode, dependency, maxversion); @@ -1491,23 +1562,28 @@ else if (!envFile.isDirectory()) envFile = envFile2; String checkDepCode; if (minversion != null && maxversion != null && minversion.equals(maxversion)) { - checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(version(pkg)) == vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major" + + " and vv.parse(platform.python_version()).minor == vv.parse(desired_version).minor else sys.exit(1)"; checkDepCode = String.format(checkDepCode, maxversion); } else if (minversion == null && maxversion == null) { - return true; + checkDepCode = "2 + 2"; } else if (maxversion == null) { - checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(version(platform.python_version())) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " + + "and vv.parse(platform.python_version()).minor %s vv.parse(desired_version).minor else sys.exit(1)"; checkDepCode = String.format(checkDepCode, minversion, strictlyBiggerOrSmaller ? ">" : ">="); } else if (minversion == null) { - checkDepCode = "import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(version(platform.python_version())) %s vv.parse(desired_version) else sys.exit(1)"; + checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " + + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " + + "and vv.parse(platform.python_version()).minor %s vv.parse(desired_version).minor else sys.exit(1)"; checkDepCode = String.format(checkDepCode, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); } else { checkDepCode = "import platform; " + "from packaging import version as vv; min_v = '%s'; max_v = '%s'; " - + "sys.exit(0) if vv.parse(platform.python_version()) %s vv.parse(min_v) and vv.parse(platform.python_version()) %s vv.parse(max_v) else sys.exit(1)"; + + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " + + "and vv.parse(platform.python_version()).minor %s vv.parse(min_v).minor " + + "and vv.parse(platform.python_version()).minor %s vv.parse(max_v).minor else sys.exit(1)"; checkDepCode = String.format(checkDepCode, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); } try { @@ -1521,7 +1597,8 @@ else if (!envFile.isDirectory()) /** * TODO figure out whether to use a dependency or not to parse the yaml file * @param envYaml - * @return + * the path to the yaml file where a Python environment should be specified + * @return true if the env exists or false otherwise * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ public boolean checkEnvFromYamlExists(String envYaml) throws MambaInstallException { @@ -1533,5 +1610,88 @@ public boolean checkEnvFromYamlExists(String envYaml) throws MambaInstallExcepti } return false; } + + /** + * In Windows, if a command prompt argument contains and space " " it needs to + * start and end with double quotes + * @param arg + * the cmd argument + * @return a robust argument + */ + private static String coverArgWithDoubleQuotes(String arg) { + String[] specialChars = new String[] {" "}; + for (String schar : specialChars) { + if (arg.startsWith("\"") && arg.endsWith("\"")) + continue; + if (arg.contains(schar) && SystemUtils.IS_OS_WINDOWS) { + return "\"" + arg + "\""; + } + } + return arg; + } + + /** + * When an argument of a command prompt argument in Windows contains an space, not + * only the argument needs to be surrounded by double quotes, but the whole sentence + * @param args + * arguments to be executed by the windows cmd + * @return a complete Sting containing all the arguments and surrounded by double quotes + */ + private static String surroundWithQuotes(List args) { + String arg = "\""; + for (String aa : args) { + arg += aa + " "; + } + arg = arg.substring(0, arg.length() - 1); + arg += "\""; + return arg; + } + + public static void main(String[] args) throws IOException, InterruptedException, MambaInstallException { + + Mamba m = new Mamba("C:\\Users\\angel\\Desktop\\Fiji app\\appose_x86_64"); + String envName = "efficientvit_sam_env"; + m.pipInstallIn(envName, new String[] {m.getEnvsDir() + File.separator + envName + + File.separator + "appose-python"}); + } + + /** + * TODO keep until release of stable Appose + * Install the Python package to run Appose in Python + * @param envName + * environment where Appose is going to be installed + * @throws IOException if there is any file creation related issue + * @throws InterruptedException if the package installation is interrupted + * @throws MambaInstallException if there is any error with the Mamba installation + */ + private void installApposeFromSource(String envName) throws IOException, InterruptedException, MambaInstallException { + checkMambaInstalled(); + if (!installed) throw new MambaInstallException("Micromamba is not installed"); + String zipResourcePath = "appose-python.zip"; + String outputDirectory = this.getEnvsDir() + File.separator + envName; + if (new File(envName).isDirectory()) outputDirectory = new File(envName).getAbsolutePath(); + try ( + InputStream zipInputStream = Mamba.class.getClassLoader().getResourceAsStream(zipResourcePath); + ZipInputStream zipInput = new ZipInputStream(zipInputStream); + ) { + ZipEntry entry; + while ((entry = zipInput.getNextEntry()) != null) { + File entryFile = new File(outputDirectory + File.separator + entry.getName()); + if (entry.isDirectory()) { + entryFile.mkdirs(); + continue; + } + entryFile.getParentFile().mkdirs(); + try (OutputStream entryOutput = new FileOutputStream(entryFile)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zipInput.read(buffer)) != -1) { + entryOutput.write(buffer, 0, bytesRead); + } + } + } + } + this.pipInstallIn(envName, new String[] {outputDirectory + File.separator + "appose-python"}); + } } From 4b2ddb5eedb81d935ea6346b6ca4296a176e0d33 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 15:06:13 -0500 Subject: [PATCH 064/120] Don't invoke mamba if no environment was requested --- src/main/java/org/apposed/appose/Builder.java | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index e275ecb..b8f780f 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -49,26 +49,19 @@ public Environment build() { // - Download and unpack JVM of the given vendor+version. // - Populate ${baseDirectory}/jars with Maven artifacts? - try { - Mamba conda = new Mamba(Mamba.BASE_PATH); - conda.installMicromamba(); - String envName = "appose"; - if (conda.getEnvironmentNames().contains( envName )) { - // TODO: Should we update it? For now, we just use it. + if (condaEnvironmentYaml != null) { + try { + Mamba conda = new Mamba(Mamba.BASE_PATH); + conda.installMicromamba(); + String envName = "appose"; + if (conda.getEnvironmentNames().contains(envName)) { + // TODO: Should we update it? For now, we just use it. + } else { + conda.createWithYaml(envName, condaEnvironmentYaml.getAbsolutePath()); + } + } catch (IOException | InterruptedException | ArchiveException | URISyntaxException | MambaInstallException e) { + throw new RuntimeException(e); } - else { - conda.createWithYaml(envName, condaEnvironmentYaml.getAbsolutePath()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ArchiveException e) { - throw new RuntimeException(e); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } catch (MambaInstallException e) { - throw new RuntimeException(e); } return new Environment() { From 78a0a3ca9797028bc5e61e92b8c994c3281b2a42 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 15:35:45 -0500 Subject: [PATCH 065/120] Fix IntelliJ warnings; do minor cleanups --- src/main/java/org/apposed/appose/Builder.java | 2 - .../java/org/apposed/appose/Environment.java | 4 +- .../org/apposed/appose/FileDownloader.java | 7 +- .../java/org/apposed/appose/GroovyWorker.java | 2 + src/main/java/org/apposed/appose/Mamba.java | 170 +++++++++--------- src/main/java/org/apposed/appose/NDArray.java | 25 +-- 6 files changed, 104 insertions(+), 106 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index e037e5d..8b0370a 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -32,13 +32,11 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; -import java.nio.file.Paths; import org.apache.commons.compress.archivers.ArchiveException; public class Builder { - public Environment build() { String base = baseDir.getPath(); boolean useSystemPath = systemPath; diff --git a/src/main/java/org/apposed/appose/Environment.java b/src/main/java/org/apposed/appose/Environment.java index 1fe7204..4891b65 100644 --- a/src/main/java/org/apposed/appose/Environment.java +++ b/src/main/java/org/apposed/appose/Environment.java @@ -172,7 +172,7 @@ default Service java(String mainClass, List classPath, * * @param exes List of executables to try for launching the worker process. * @param args Command line arguments to pass to the worker process - * (e.g. {"-v", "--enable-everything"}. + * (e.g. {"-v", "--enable-everything"}). * @return The newly created service. * @see #groovy To create a service for Groovy script execution. * @see #python() To create a service for Python script execution. @@ -183,7 +183,7 @@ default Service service(List exes, String... args) throws IOException { List dirs = useSystemPath() // ? Arrays.asList(System.getenv("PATH").split(File.pathSeparator)) // - : Arrays.asList(base()); + : Collections.singletonList(base()); File exeFile = FilePaths.findExe(dirs, exes); if (exeFile == null) throw new IllegalArgumentException("No executables found amongst candidates: " + exes); diff --git a/src/main/java/org/apposed/appose/FileDownloader.java b/src/main/java/org/apposed/appose/FileDownloader.java index b659d8c..83cc956 100644 --- a/src/main/java/org/apposed/appose/FileDownloader.java +++ b/src/main/java/org/apposed/appose/FileDownloader.java @@ -5,10 +5,11 @@ import java.nio.channels.ReadableByteChannel; public class FileDownloader { - private ReadableByteChannel rbc; - private FileOutputStream fos; private static final long CHUNK_SIZE = 1024 * 1024 * 5; - + + private final ReadableByteChannel rbc; + private final FileOutputStream fos; + public FileDownloader(ReadableByteChannel rbc, FileOutputStream fos) { this.rbc = rbc; this.fos = fos; diff --git a/src/main/java/org/apposed/appose/GroovyWorker.java b/src/main/java/org/apposed/appose/GroovyWorker.java index 0ac4b7f..585d624 100644 --- a/src/main/java/org/apposed/appose/GroovyWorker.java +++ b/src/main/java/org/apposed/appose/GroovyWorker.java @@ -118,6 +118,7 @@ public Task(String uuid) { this.uuid = uuid; } + @SuppressWarnings("unused") public void update(String message, Long current, Long maximum) { Map args = new HashMap<>(); if (message != null) args.put("message", message); @@ -126,6 +127,7 @@ public void update(String message, Long current, Long maximum) { respond(ResponseType.UPDATE, args); } + @SuppressWarnings("unused") public void cancel() { respond(ResponseType.CANCELATION, null); } diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 1a3ea74..3f49e96 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -35,7 +35,6 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -51,6 +50,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -112,11 +112,11 @@ public class Mamba { * Consumer that tracks the progress in the download of micromamba, the software used * by this class to manage Python environments */ - private Consumer mambaDnwldProgressConsumer = this::updateMambaDnwldProgress; + private final Consumer mambaDnwldProgressConsumer = this::updateMambaDnwldProgress; /** * Consumer that tracks the progress decompressing the downloaded micromamba files. */ - private Consumer mambaDecompressProgressConsumer = this::updateMambaDecompressProgress; + private final Consumer mambaDecompressProgressConsumer = this::updateMambaDecompressProgress; /** * String that contains all the console output produced by micromamba ever since the {@link Mamba} was instantiated */ @@ -137,12 +137,12 @@ public class Mamba { * Consumer that tracks the console output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer consoleConsumer = this::updateConsoleConsumer; + private final Consumer consoleConsumer = this::updateConsoleConsumer; /** * Consumer that tracks the error output produced by the micromamba process when it is executed. * This consumer saves all the log of every micromamba execution */ - private Consumer errConsumer = this::updateErrorConsumer; + private final Consumer errConsumer = this::updateErrorConsumer; /* * Path to Python executable from the environment directory */ @@ -240,9 +240,9 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) * Create a new {@link Mamba} object. The root dir for the Micromamba installation * will be the default base path defined at {@link #BASE_PATH} * If there is no Micromamba found at the base path {@link #BASE_PATH}, a {@link MambaInstallException} will be thrown - * + *

* It is expected that the Micromamba installation has executable commands as shown below: - * + *

*
 	 * MAMBA_ROOT
 	 * ├── bin
@@ -252,7 +252,6 @@ private ProcessBuilder getBuilder( final boolean isInheritIO )
 	 * │   ├── your_env
 	 * │   │   ├── python(.exe)
 	 * 
- * */ public Mamba() { this(BASE_PATH); @@ -263,9 +262,9 @@ public Mamba() { * specified as {@code String}. * If there is no Micromamba found at the specified path, it will be installed automatically * if the parameter 'installIfNeeded' is true. If not a {@link MambaInstallException} will be thrown. - * + *

* It is expected that the Conda installation has executable commands as shown below: - * + *

*
 	 * MAMBA_ROOT
 	 * ├── bin
@@ -279,7 +278,7 @@ public Mamba() {
 	 * @param rootdir
 	 *  The root dir for Mamba installation.
 	 */
-	public Mamba( final String rootdir) {
+	public Mamba(final String rootdir) {
 		if (rootdir == null)
 			this.rootdir = BASE_PATH;
 		else
@@ -372,8 +371,8 @@ private File downloadMicromamba() throws IOException, URISyntaxException {
 		Thread dwnldThread = new Thread(() -> {
 			try (
 					ReadableByteChannel rbc = Channels.newChannel(website.openStream());
-					FileOutputStream fos = new FileOutputStream(tempFile);
-					) {
+					FileOutputStream fos = new FileOutputStream(tempFile)
+			) {
 				new FileDownloader(rbc, fos).call(currentThread);
 			} catch (IOException | InterruptedException e) {
 				e.printStackTrace();
@@ -388,7 +387,7 @@ private File downloadMicromamba() throws IOException, URISyntaxException {
 	}
 	
 	private void decompressMicromamba(final File tempFile) 
-				throws FileNotFoundException, IOException, ArchiveException, InterruptedException {
+				throws IOException, ArchiveException, InterruptedException {
 		final File tempTarFile = File.createTempFile( "micromamba", ".tar" );
 		tempTarFile.deleteOnExit();
 		MambaInstallerUtils.unBZip2(tempFile, tempTarFile);
@@ -433,7 +432,6 @@ public String getEnvsDir() {
 	 * 
 	 * @return {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for
 	 *         Mac/Linux.
-	 * @throws IOException
 	 */
 	private static List< String > getBaseCommand()
 	{
@@ -489,7 +487,7 @@ public void updateIn( final String envName, final String... args ) throws IOExce
 		final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", this.envsdir + File.separator + envName ) );
 		cmd.addAll( Arrays.asList( args ) );
 		if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes");
-		runMamba( cmd.stream().toArray( String[]::new ) );
+		runMamba(cmd.toArray(new String[0]));
 	}
 
 	/**
@@ -521,8 +519,6 @@ public void createWithYaml( final String envName, final String envYaml ) throws
 	 *            The environment name to be created. It should not be a path, just the name.
 	 * @param envYaml
 	 *            The environment yaml file containing the information required to build it  
-	 * @param envName
-	 *            The environment name to be created.
 	 * @param isForceCreation
 	 *            Force creation of the environment if {@code true}. If this value
 	 *            is {@code false} and an environment with the specified name
@@ -655,7 +651,7 @@ public void create( final String envName, final boolean isForceCreation, final S
 		final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) );
 		cmd.addAll( Arrays.asList( args ) );
 		if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes");
-		runMamba( cmd.stream().toArray( String[]::new ) );
+		runMamba(cmd.toArray(new String[0]));
 		if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python"))
 			installApposeFromSource(envsdir + File.separator + envName);
 	}
@@ -693,12 +689,12 @@ public void create( final String envName, final boolean isForceCreation, List cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) );
-		if (channels == null) channels = new ArrayList();
+		if (channels == null) channels = new ArrayList<>();
 		for (String chan : channels) { cmd.add("-c"); cmd.add(chan);}
-		if (packages == null) packages = new ArrayList();
-		for (String pack : packages) { cmd.add(pack);}
+		if (packages == null) packages = new ArrayList<>();
+		cmd.addAll(packages);
 		if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes");
-		runMamba( cmd.stream().toArray( String[]::new ) );
+		runMamba(cmd.toArray(new String[0]));
 		if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python"))
 			installApposeFromSource(envsdir + File.separator + envName);
 	}
@@ -713,7 +709,7 @@ public void create( final String envName, final boolean isForceCreation, List channels, List
 		if (!installed) throw new MambaInstallException("Micromamba is not installed");
 		Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided.");		
 		final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) );
-		if (channels == null) channels = new ArrayList();
+		if (channels == null) channels = new ArrayList<>();
 		for (String chan : channels) { cmd.add("-c"); cmd.add(chan);}
-		if (packages == null) packages = new ArrayList();
-		for (String pack : packages) { cmd.add(pack);}
-		runMamba( cmd.stream().toArray( String[]::new ) );
+		if (packages == null) packages = new ArrayList<>();
+		cmd.addAll(packages);
+		runMamba(cmd.toArray(new String[0]));
 	}
 
 	/**
@@ -862,7 +858,7 @@ public void installIn( final String envName, final String... args ) throws IOExc
 		final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", this.envsdir + File.separator + envName ) );
 		cmd.addAll( Arrays.asList( args ) );
 		if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes");
-		runMamba( cmd.stream().toArray( String[]::new ) );
+		runMamba(cmd.toArray(new String[0]));
 	}
 
 	/**
@@ -910,7 +906,7 @@ public void pipInstallIn( final String envName, final String... args ) throws IO
 		if (!installed) throw new MambaInstallException("Micromamba is not installed");
 		final List< String > cmd = new ArrayList<>( Arrays.asList( "-m", "pip", "install" ) );
 		cmd.addAll( Arrays.asList( args ) );
-		runPythonIn( envName, cmd.stream().toArray( String[]::new ) );
+		runPythonIn( envName, cmd.toArray(new String[0]));
 	}
 
 	/**
@@ -937,17 +933,18 @@ public void runPython( final String... args ) throws IOException, InterruptedExc
 	}
 
 	/**
+	 * Runs a Python command in the specified environment. This method automatically
+	 * sets environment variables associated with the specified environment. In
+	 * Windows, this method also sets the {@code PATH} environment variable so that
+	 * the specified environment runs as expected.
+	 * 

* TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - * - * Run a Python command in the specified environment. This method automatically - * sets environment variables associated with the specified environment. In - * Windows, this method also sets the {@code PATH} environment variable so that - * the specified environment runs as expected. - * + *

+ * * @param envName * The environment name used to run the Python command. * @param args @@ -965,7 +962,7 @@ public void runPythonIn( final String envName, final String... args ) throws IOE checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = getBaseCommand(); - List argsList = new ArrayList(); + List argsList = new ArrayList<>(); String envDir; if (new File(envName, PYTHON_COMMAND).isFile()) { argsList.add( coverArgWithDoubleQuotes(Paths.get( envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); @@ -976,11 +973,11 @@ public void runPythonIn( final String envName, final String... args ) throws IOE } else throw new IOException("The environment provided (" + envName + ") does not exist or does not contain a Python executable (" + PYTHON_COMMAND + ")."); - argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + argsList.addAll( Arrays.stream( args ).map(aa -> { if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); else return aa; }).collect(Collectors.toList()) ); - boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); else cmd.add(surroundWithQuotes(argsList)); @@ -990,9 +987,9 @@ public void runPythonIn( final String envName, final String... args ) throws IOE { final Map< String, String > envs = builder.environment(); envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library", "Bin" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Scripts" ) + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library" ) + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library", "Bin" ) + ";" + envs.get( "Path" ) ); } // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); if ( builder.command( cmd ).start().waitFor() != 0 ) @@ -1022,13 +1019,13 @@ public static void runPythonIn( final File envFile, final String... args ) throw throw new IOException("No Python found in the environment provided. The following " + "file does not exist: " + Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath()); final List< String > cmd = getBaseCommand(); - List argsList = new ArrayList(); + List argsList = new ArrayList<>(); argsList.add( coverArgWithDoubleQuotes(Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath().toString()) ); - argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + argsList.addAll( Arrays.stream( args ).map(aa -> { if (Platform.isWindows() && aa.contains(" ")) return coverArgWithDoubleQuotes(aa); else return aa; }).collect(Collectors.toList()) ); - boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); else cmd.add(surroundWithQuotes(argsList)); @@ -1041,9 +1038,9 @@ public static void runPythonIn( final File envFile, final String... args ) throw final Map< String, String > envs = builder.environment(); final String envDir = envFile.getAbsolutePath(); envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Scripts" ).toString() + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library" ).toString() + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library", "Bin" ).toString() + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Scripts" ) + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library" ) + ";" + envs.get( "Path" ) ); + envs.put( "Path", Paths.get( envDir, "Library", "Bin" ) + ";" + envs.get( "Path" ) ); } // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); if ( builder.command( cmd ).start().waitFor() != 0 ) @@ -1060,10 +1057,8 @@ public static void runPythonIn( final File envFile, final String... args ) throw * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public String getVersion() throws IOException, InterruptedException, MambaInstallException - { + public String getVersion() throws IOException, InterruptedException { final List< String > cmd = getBaseCommand(); if (mambaCommand.contains(" ") && SystemUtils.IS_OS_WINDOWS) cmd.add( surroundWithQuotes(Arrays.asList( coverArgWithDoubleQuotes(mambaCommand), "--version" )) ); @@ -1101,13 +1096,13 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); final List< String > cmd = getBaseCommand(); - List argsList = new ArrayList(); + List argsList = new ArrayList<>(); argsList.add( coverArgWithDoubleQuotes(mambaCommand) ); - argsList.addAll( Arrays.asList( args ).stream().map(aa -> { + argsList.addAll( Arrays.stream( args ).map(aa -> { if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); else return aa; }).collect(Collectors.toList()) ); - boolean containsSpaces = argsList.stream().filter(aa -> aa.contains(" ")).collect(Collectors.toList()).size() > 0; + boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); else cmd.add(surroundWithQuotes(argsList)); @@ -1120,12 +1115,12 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE Thread outputThread = new Thread(() -> { try ( InputStream inputStream = process.getInputStream(); - InputStream errStream = process.getErrorStream(); + InputStream errStream = process.getErrorStream() ){ byte[] buffer = new byte[1024]; // Buffer size can be adjusted StringBuilder processBuff = new StringBuilder(); StringBuilder errBuff = new StringBuilder(); - String processChunk = ""; + String processChunk = ""; String errChunk = ""; int newLineIndex; long t0 = System.currentTimeMillis(); @@ -1137,8 +1132,8 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE if (inputStream.available() > 0) { processBuff.append(new String(buffer, 0, inputStream.read(buffer))); while ((newLineIndex = processBuff.indexOf(System.lineSeparator())) != -1) { - processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " - + processBuff.substring(0, newLineIndex + 1).trim() + System.lineSeparator(); + processChunk += sdf.format(Calendar.getInstance().getTime()) + " -- " + + processBuff.substring(0, newLineIndex + 1).trim() + System.lineSeparator(); processBuff.delete(0, newLineIndex + 1); } } @@ -1279,7 +1274,7 @@ public List< String > getEnvironmentNames() throws IOException, MambaInstallExce { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > envs = new ArrayList<>( Arrays.asList( DEFAULT_ENVIRONMENT_NAME ) ); + final List< String > envs = new ArrayList<>(Collections.singletonList(DEFAULT_ENVIRONMENT_NAME)); envs.addAll( Files.list( Paths.get( envsdir ) ) .map( p -> p.getFileName().toString() ) .filter( p -> !p.startsWith( "." ) ) @@ -1304,7 +1299,7 @@ public List< String > getEnvironmentNames() throws IOException, MambaInstallExce public boolean checkAllDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - return checkUninstalledDependenciesInEnv(envName, dependencies).size() == 0; + return checkUninstalledDependenciesInEnv(envName, dependencies).isEmpty(); } /** @@ -1319,7 +1314,7 @@ public boolean checkAllDependenciesInEnv(String envName, List dependenci * They can contain version requirements. The names should be the ones used to import the package inside python, * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" - * @return true if the packages are installed or false otherwise + * @return the list of packages that are not already installed * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ public List checkUninstalledDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { @@ -1329,14 +1324,13 @@ public List checkUninstalledDependenciesInEnv(String envName, List uninstalled = dependencies.stream().filter(dep -> { + return dependencies.stream().filter(dep -> { try { return !checkDependencyInEnv(envName, dep); } catch (Exception ex) { return true; } }).collect(Collectors.toList()); - return uninstalled; } /** @@ -1604,7 +1598,7 @@ else if (!envFile.isDirectory()) public boolean checkEnvFromYamlExists(String envYaml) throws MambaInstallException { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if (envYaml == null || new File(envYaml).isFile() == false + if (envYaml == null || !new File(envYaml).isFile() || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { return false; } @@ -1651,8 +1645,8 @@ public static void main(String[] args) throws IOException, InterruptedException, Mamba m = new Mamba("C:\\Users\\angel\\Desktop\\Fiji app\\appose_x86_64"); String envName = "efficientvit_sam_env"; - m.pipInstallIn(envName, new String[] {m.getEnvsDir() + File.separator + envName - + File.separator + "appose-python"}); + m.pipInstallIn(envName, + m.getEnvsDir() + File.separator + envName + File.separator + "appose-python"); } /** @@ -1670,28 +1664,28 @@ private void installApposeFromSource(String envName) throws IOException, Interru String zipResourcePath = "appose-python.zip"; String outputDirectory = this.getEnvsDir() + File.separator + envName; if (new File(envName).isDirectory()) outputDirectory = new File(envName).getAbsolutePath(); - try ( - InputStream zipInputStream = Mamba.class.getClassLoader().getResourceAsStream(zipResourcePath); - ZipInputStream zipInput = new ZipInputStream(zipInputStream); - ) { - ZipEntry entry; - while ((entry = zipInput.getNextEntry()) != null) { - File entryFile = new File(outputDirectory + File.separator + entry.getName()); - if (entry.isDirectory()) { - entryFile.mkdirs(); - continue; - } - entryFile.getParentFile().mkdirs(); - try (OutputStream entryOutput = new FileOutputStream(entryFile)) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = zipInput.read(buffer)) != -1) { - entryOutput.write(buffer, 0, bytesRead); - } - } - } - } - this.pipInstallIn(envName, new String[] {outputDirectory + File.separator + "appose-python"}); + try ( + InputStream zipInputStream = Mamba.class.getClassLoader().getResourceAsStream(zipResourcePath); + ZipInputStream zipInput = new ZipInputStream(zipInputStream) + ) { + ZipEntry entry; + while ((entry = zipInput.getNextEntry()) != null) { + File entryFile = new File(outputDirectory + File.separator + entry.getName()); + if (entry.isDirectory()) { + entryFile.mkdirs(); + continue; + } + entryFile.getParentFile().mkdirs(); + try (OutputStream entryOutput = new FileOutputStream(entryFile)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zipInput.read(buffer)) != -1) { + entryOutput.write(buffer, 0, bytesRead); + } + } + } + } + this.pipInstallIn(envName, outputDirectory + File.separator + "appose-python"); } } diff --git a/src/main/java/org/apposed/appose/NDArray.java b/src/main/java/org/apposed/appose/NDArray.java index 62e19b4..525b915 100644 --- a/src/main/java/org/apposed/appose/NDArray.java +++ b/src/main/java/org/apposed/appose/NDArray.java @@ -143,7 +143,8 @@ private static int safeInt(final long value) { /** * Enumerates possible data type of {@link NDArray} elements. */ - public static enum DType { + @SuppressWarnings("unused") + public enum DType { INT8("int8", Byte.BYTES), // INT16("int16", Short.BYTES), // INT32("int32", Integer.BYTES), // @@ -179,8 +180,8 @@ public int bytesPerElement() /** * Get the label of this {@code DType}. *

- * The label can used as a {@code dtype} in Python. It is also used for JSON - * serialization. + * The label can be used as a {@code dtype} in Python. + * It is also used for JSON serialization. * * @return the label. */ @@ -203,7 +204,7 @@ public static DType fromLabel(final String label) throws IllegalArgumentExceptio } /** - * The shape of a multi-dimensional array. + * The shape of a multidimensional array. */ public static class Shape { @@ -282,6 +283,7 @@ public long numElements() { * * @return dimensions array */ + @SuppressWarnings("unused") public int[] toIntArray() { return shape; } @@ -296,9 +298,9 @@ public int[] toIntArray(final Order order) { return shape; } else { - final int[] ishape = new int[shape.length]; - Arrays.setAll(ishape, i -> shape[shape.length - i - 1]); - return ishape; + final int[] iShape = new int[shape.length]; + Arrays.setAll(iShape, i -> shape[shape.length - i - 1]); + return iShape; } } @@ -308,6 +310,7 @@ public int[] toIntArray(final Order order) { * * @return dimensions array */ + @SuppressWarnings("unused") public long[] toLongArray() { return toLongArray(order); } @@ -318,14 +321,14 @@ public long[] toLongArray() { * @return dimensions array */ public long[] toLongArray(final Order order) { - final long[] lshape = new long[shape.length]; + final long[] lShape = new long[shape.length]; if (order.equals(this.order)) { - Arrays.setAll(lshape, i -> shape[i]); + Arrays.setAll(lShape, i -> shape[i]); } else { - Arrays.setAll(lshape, i -> shape[shape.length - i - 1]); + Arrays.setAll(lShape, i -> shape[shape.length - i - 1]); } - return lshape; + return lShape; } /** From 9cdf301237aae5071ddce52575d2397140a7e8b5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 13 Aug 2024 17:39:11 -0500 Subject: [PATCH 066/120] Remove pinned ivy version Version 38.0.1 of the pom-scijava parent brings in Ivy 2.5.2, so the pin is no longer necessary. And Ivy 2.5.1 suffers from CVE-2022-46751. --- pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pom.xml b/pom.xml index ed0093a..c88f64d 100644 --- a/pom.xml +++ b/pom.xml @@ -88,8 +88,6 @@ bsd_2 Appose developers. - - 2.5.1 From f2a974d4b82cb074e82ed3692e3b3a5a2dea0c69 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 Aug 2024 15:14:10 -0500 Subject: [PATCH 067/120] Add aspirational conda builder test, for TDD --- .../java/org/apposed/appose/ApposeTest.java | 28 ++++++++++++++----- src/test/resources/envs/cowsay.yml | 9 ++++++ 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/test/resources/envs/cowsay.yml diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index e4322a6..b2107b6 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -87,14 +87,28 @@ public void testPython() throws IOException, InterruptedException { } } - public void testConda() { - Environment env = Appose.conda(new File("appose-environment.yml")).build(); + @Test + public void testConda() throws IOException, InterruptedException { + Environment env = Appose.conda(new File("src/test/resources/envs/cowsay.yml")).build(); try (Service service = env.python()) { - service.debug(System.err::println); - executeAndAssert(service, "import cowsay; "); - } catch (IOException | InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Task task = service.task( + "import cowsay\n" + + "moo = cowsay.get_output_string(\"cow\", \"moo\")\n" + ); + task.waitFor(); + String expectedMoo = + " ___\n" + + "| moo |\n" + + "===\n" + + " \\\n" + + " \\\n" + + " ^__^\n" + + " (oo)\\_______\n" + + " (__)\\ )\\/\\\n" + + " ||----w |\n" + + " || ||"; + String actualMoo = (String) task.outputs.get("moo"); + assertEquals(expectedMoo, actualMoo); } } diff --git a/src/test/resources/envs/cowsay.yml b/src/test/resources/envs/cowsay.yml new file mode 100644 index 0000000..a35704e --- /dev/null +++ b/src/test/resources/envs/cowsay.yml @@ -0,0 +1,9 @@ +name: appose-cowsay +channels: + - conda-forge +dependencies: + - python >= 3.8 + - pip + - appose + - pip: + - cowsay==6.1 From 7a8091f357781664143c236517ccc98dbe44f8a0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 16 Aug 2024 06:28:18 -0500 Subject: [PATCH 068/120] POM: bump major/minor version The Mamba logic is new. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c88f64d..719d553 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.apposed appose - 0.2.1-SNAPSHOT + 0.3.0-SNAPSHOT Appose Appose: multi-language interprocess cooperation with shared memory. From 62106976b163de53be7a60b10ed5051aa240a0ed Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 16 Aug 2024 06:29:06 -0500 Subject: [PATCH 069/120] Fix up the license headers --- .../org/apposed/appose/CondaException.java | 28 ++++++++++++++++ .../org/apposed/appose/FileDownloader.java | 28 ++++++++++++++++ src/main/java/org/apposed/appose/Mamba.java | 33 +++++++++++++++++++ .../apposed/appose/MambaInstallException.java | 28 ++++++++++++++++ .../apposed/appose/MambaInstallerUtils.java | 2 +- 5 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/CondaException.java b/src/main/java/org/apposed/appose/CondaException.java index 632fc7b..2efc7b3 100644 --- a/src/main/java/org/apposed/appose/CondaException.java +++ b/src/main/java/org/apposed/appose/CondaException.java @@ -1,3 +1,31 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.apposed.appose; public class CondaException diff --git a/src/main/java/org/apposed/appose/FileDownloader.java b/src/main/java/org/apposed/appose/FileDownloader.java index 83cc956..bcb0f5a 100644 --- a/src/main/java/org/apposed/appose/FileDownloader.java +++ b/src/main/java/org/apposed/appose/FileDownloader.java @@ -1,3 +1,31 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.apposed.appose; import java.io.FileOutputStream; diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 3f49e96..e0bad74 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -1,3 +1,35 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +// Adapted from JavaConda (https://github.com/javaconda/javaconda), +// which has the following license: + /******************************************************************************* * Copyright (C) 2021, Ko Sugawara * All rights reserved. @@ -24,6 +56,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ + package org.apposed.appose; import org.apache.commons.compress.archivers.ArchiveException; diff --git a/src/main/java/org/apposed/appose/MambaInstallException.java b/src/main/java/org/apposed/appose/MambaInstallException.java index 33f856e..efbd5d6 100644 --- a/src/main/java/org/apposed/appose/MambaInstallException.java +++ b/src/main/java/org/apposed/appose/MambaInstallException.java @@ -1,3 +1,31 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.apposed.appose; /** diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/MambaInstallerUtils.java index 4bd5c71..3b7949e 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/MambaInstallerUtils.java @@ -2,7 +2,7 @@ * #%L * Appose: multi-language interprocess cooperation with shared memory. * %% - * Copyright (C) 2023 Appose developers. + * Copyright (C) 2023 - 2024 Appose developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: From e52a7ccf37b6a04ad13fe69085b7b7790c18b52f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 15:37:04 -0500 Subject: [PATCH 070/120] Add notes on how the Builder API should work --- notes.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 notes.md diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..cbb3b98 --- /dev/null +++ b/notes.md @@ -0,0 +1,234 @@ +## Builder API + +* Want an API to create an environment from an envFile: i.e. `pixi.toml` or `environment.yml`. +* Want an API to build up an environment piecemeal: adding dependencies one by one. +* Do we need an API to mix and match these two things? I.e. start from envFile but then add on? + - An argument for this: what if you want to mix in Java JARs? pixi.toml can't do that yet. +* Want an API to create an environment from a *string* representation of an envFile. + +Most flexible to put all these things into the Builder, not only directly in Appose (like `system()`). + +What sorts of dependencies do we want to support adding? + +1. conda-forge packages +2. PyPI packages +3. Maven coords +4. Java itself + +Pixi gets us (1) and (2). + +* Maven coords can be gotten by Groovy Grape in Java, by jgo in Python. (jgo needs work) +* Java itself can be gotten by cjdk in Python; what about from Java? Port cjdk? Or hack it with openjdk conda for now? + +Should we make the API more general than the above? Yes! We can use `ServiceLoader`, same as we do with `ShmFactory`. + +The interface is `BuildHandler`: +* `boolean include(String content, String scheme)` +* `boolean channel(String name, String location)` + +And the implementations + supported schemes are: +* `PixiHandler` -- `environment.yml`, `pixi.toml`, `pypi`, `conda`, and null. +* `MavenHandler` -- `maven` +* `OpenJDKHandler` -- `openjdk` + +Although the term "scheme" might be confused with URI scheme, other terms are problematic too: +* "platform" will be confused with OS/arch. +* "method" will be confused with functions of a class. +* "system" might be confused with the computer itself, and/or system environment, path, etc. +* "paradigm" sounds too pretentious. +* "repoType" is rather clunky. + +The `Builder` then has its own `include` and `channel` methods that delegate to +all discovered `BuildHandler` plugins. The `Builder` can also have more +convenience methods: + +* `Builder file(String filePath) { return file(new File(filePath)); }` +* `Builder file(String filePath, String scheme) { return file(new File(filePath), scheme); }` +* `Builder file(File file) { return file(file, file.getName()); }` +* `Builder file(File file, String scheme) { return include(readContentsAsString(file), scheme); }` + +For the `file`-to-`include` trick to work with files like `requirements.txt`, +the handling of `conda`/null scheme should split the content string into lines, +and process them in a loop. + +Here are some example API calls made possible by the above design: +```java +Appose.env() + .file("/path/to/environment.yml") + // OR: .file("/path/to/pixi.toml") + // OR: .file("/path/to/requirements.txt", "pypi") + .include("cowsay", "pypi") + .include("openjdk>=17") // Install OpenJDK from conda-forge! + .include("maven") // Install Maven from conda-forge... confusing, yeah? + .include("conda-forge::maven") // Specify channel explicitly with environment.yml syntax. + .include("org.scijava:parsington", "maven") + // OR: .include("org.scijava:parsington") i.e. infer `maven` from the colon? + // OR: .include("org.scijava:parsington:2.0.0", "maven") + .channel("scijava", "maven:https://maven.scijava.org/content/groups/public") + .include("sc.fiji:fiji", "maven") + .include("zulu:17", "openjdk") // Install an OpenJDK from the Coursier index. + + .channel("bioconda") // Add a conda channel + .channel(name: str, location: str = None) + .build() // Whew! +``` + +## Pixi + +Is even better than micromamba. It's a great fit for Appose's requirements. + +### Setup for Appose + +```shell +# Install a copy of Pixi into Appose's workspace. +mkdir -p ~/.local/share/appose/tmp +cd ~/.local/share/appose/tmp +curl -fsLO https://github.com/prefix-dev/pixi/releases/download/v0.27.1/pixi-x86_64-unknown-linux-musl.tar.gz +mkdir -p ../.pixi/bin +cd ../.pixi/bin +tar xf ../tmp/pixi-x86_64-unknown-linux-musl.tar.gz +alias pixi=~/.local/share/appose/.pixi/bin/pixi +``` + +And/or consider setting `PIXI_HOME` to `$HOME/.local/share/appose` +when using the `$HOME/.local/share/appose/.pixi/bin/pixi` binary. +This would let us, in the future, tweak Pixi's Appose-wide configuration +by adding a `$HOME/.local/share/appose/.pixi/config.toml` file. + +#### Create an Appose environment + +```shell +mkdir -p ~/.local/share/appose/sc-fiji-spiff +pixi init ~/.local/share/appose/sc-fiji-spiff +``` + +#### Add channels to the project/environment + +```shell +cd ~/.local/share/appose/sc-fiji-spiff +pixi project channel add bioconda pytorch +``` + +Doing them all in one command will have less overhead. + +#### Add dependencies to the project/environment + +```shell +cd ~/.local/share/appose/sc-fiji-spiff +pixi add python pip +pixi add --pypi cowsay +``` + +Doing them all in two commands (one for conda, one for pypi) will have less overhead. + +#### Use it + +```shell +pixi run python -c 'import cowsay; cowsay.cow("moo")' +``` + +One awesome thing is that Appose will be able to launch the +child process using `pixi run ...`, which takes care of running +activation scripts before launch—so the child program should +work as though run from an activated environment (or `pixi shell`). + +### Bugs + +#### Invalid environment names do not fail fast + +```shell +pixi project environment add sc.fiji.spiff +pixi tree +``` +Fails with: `Failed to parse environment name 'sc.fiji.spiff', please use only lowercase letters, numbers and dashes` + +### Multiple environments in one pixi project? + +I explored making a single Appose project and using pixi's multi-environment +support to manage Appose environments, all within that one project, as follows: + +```shell +# Initialize the shared Appose project. +pixi init +pixi project description set "Appose: multi-language interprocess cooperation with shared memory." + +# Create a new environment within the Appose project. +pixi project environment add sc-fiji-spiff +# Install dependencies into a feature with matching name. +pixi add --feature sc-fiji-spiff python pip +pixi add --feature sc-fiji-spiff --pypi cowsay +# No known command to link sc-fiji-spiff feature with sc-fiji-spiff project... +mv pixi.toml pixi.toml.old +sed 's/sc-fiji-spiff = \[\]/sc-fiji-spiff = ["sc-fiji-spiff"]/' pixi.toml.old > pixi.toml +# Finally, we can use the environment! +pixi run --environment sc-fiji-spiff python -c 'import cowsay; cowsay.cow("moo")' +``` + +This works, but a single `pixi.toml` file for all of Appose is probably too +fragile, whereas a separate project folder for each Appose environment should +be more robust, reducing the chance that one Appose-based project (e.g. JDLL) +might stomp on another Appose-based project (e.g. TrackMate) due to their usage +of the same `pixi.toml`. + +So we'll just settle for pixi's standard behavior here: a single environment +named `default` per pixi project, with one pixi project per Appose environment. +Unfortunately, that means our environment directory structure will be: +``` +~/.local/share/appose/sc-fiji-spiff/envs/default +``` +for an Appose environment named `sc-fiji-spiff`. +(Note that environment names cannot contain dots, only alphameric and dash.) + +To attain a better structure, I tried creating a `~/.local/share/appose/sc-fiji-spiff/.pixi/config.toml` with contents: +```toml +detached-environments: "/home/curtis/.local/share/appose" +``` + +It works, but then the environment folder from above ends up being: +``` +~/.local/share/appose/sc-fiji-spiff-/envs/sc-fiji-spiff +``` +Looks like pixi creates one folder under `envs` for each project, with a +numerical hash to reduce collisions between multiple projects with the same +name... and then still makes an `envs` folder for that project beneath it. +So there is no escape from pixi's directory convention of: +``` +/envs/ +``` +Which of these is the least annoying? +``` +~/.local/share/appose/sc-fiji-spiff/envs/default +~/.local/share/appose/sc-fiji-spiff-782634298734/envs/default +~/.local/share/appose/sc-fiji-spiff/envs/sc-fiji-spiff +~/.local/share/appose/sc-fiji-spiff-782634298734/envs/sc-fiji-spiff +``` +The detached-environments approach is actually longer, and entails +additional configuration and more potential for confusion; the +shortest path ends up being the first one, which is pixi's standard +behavior anyway. + +The only shorter one would be: +``` +~/.local/share/appose/envs/sc-fiji-spiff +``` +if we opted to keep `~/.local/share/appose` as a single Pixi project +root with multiple environments... but the inconvenience and risks +around a single shared `pixi.toml`, and hassle of multi-environment +configuration, outweigh the benefit of slightly shorter paths. + +With separate Pixi projects we can also let Appose users specify their own +`pixi.toml` (maybe a partial one?), directly. Or an `environment.yml` that gets +used via `pixi init --import`. Maybe someday even a `requirements.txt`, if the +request (https://github.com/prefix-dev/pixi/issues/1410) gets implemented. + +## Next steps + +1. Add tests for the current Mamba builder. +2. Make the tests pass. +3. Introduce `BuildHandler` design and migrate Mamba logic to a build handler. +4. Implement a build handler built on pixi, to replace the micromamba one. +5. Implement build handlers for maven and openjdk. +6. Implement pixi, maven, and openjdk build handlers in appose-python, too. +7. Once it all works: release 0.3.0. + +And: update https://github.com/imglib/imglib2-appose to work with appose 0.2.0+. From c5efc9ee5a279d654044e513a6cbb3f253f14f3c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 16 Aug 2024 15:42:39 -0500 Subject: [PATCH 071/120] Remove unused java(vendor, version) builder method --- src/main/java/org/apposed/appose/Appose.java | 4 ---- src/main/java/org/apposed/appose/Builder.java | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/src/main/java/org/apposed/appose/Appose.java b/src/main/java/org/apposed/appose/Appose.java index 8bf7dd4..b12be19 100644 --- a/src/main/java/org/apposed/appose/Appose.java +++ b/src/main/java/org/apposed/appose/Appose.java @@ -160,10 +160,6 @@ public static Builder base(String directory) { return base(new File(directory)); } - public static Builder java(String vendor, String version) { - return new Builder().java(vendor, version); - } - public static Builder conda(File environmentYaml) { return new Builder().conda(environmentYaml); } diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 8b0370a..cbfcef2 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -92,15 +92,4 @@ public Builder conda(File environmentYaml) { this.condaEnvironmentYaml = environmentYaml; return this; } - - // -- Java -- - - private String javaVendor; - private String javaVersion; - - public Builder java(String vendor, String version) { - this.javaVendor = vendor; - this.javaVersion = version; - return this; - } } From d2463ba7626c7c519fd834c0a1a67fc575d6bc0e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 16 Aug 2024 15:44:45 -0500 Subject: [PATCH 072/120] Disallow direct construction of Builder objects Environments should be built via the Appose utility class. --- src/main/java/org/apposed/appose/Builder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index cbfcef2..c59128a 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -37,6 +37,10 @@ public class Builder { + Builder() { + // Prevent external instantiation. + } + public Environment build() { String base = baseDir.getPath(); boolean useSystemPath = systemPath; From 405d27fbe5d5ec730801d70c089bfb714117a4c0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 19 Aug 2024 17:37:50 -0500 Subject: [PATCH 073/120] Fix bugs in the conda builder test --- .../java/org/apposed/appose/ApposeTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index b2107b6..263180d 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -93,20 +93,20 @@ public void testConda() throws IOException, InterruptedException { try (Service service = env.python()) { Task task = service.task( "import cowsay\n" + - "moo = cowsay.get_output_string(\"cow\", \"moo\")\n" + "task.outputs['moo'] = cowsay.get_output_string('cow', 'moo')\n" ); task.waitFor(); String expectedMoo = " ___\n" + "| moo |\n" + - "===\n" + - " \\\n" + - " \\\n" + - " ^__^\n" + - " (oo)\\_______\n" + - " (__)\\ )\\/\\\n" + - " ||----w |\n" + - " || ||"; + " ===\n" + + " \\\n" + + " \\\n" + + " ^__^\n" + + " (oo)\\_______\n" + + " (__)\\ )\\/\\\n" + + " ||----w |\n" + + " || ||"; String actualMoo = (String) task.outputs.get("moo"); assertEquals(expectedMoo, actualMoo); } From 893c7fbe769ab9a97a0fd85bbad95c8d6caad855 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 19 Aug 2024 17:42:39 -0500 Subject: [PATCH 074/120] Remove "install appose from source" hack Appose 0.2.0 has been released, which works fine. --- src/main/java/org/apposed/appose/Mamba.java | 48 --------------------- 1 file changed, 48 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index e0bad74..99fa275 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -576,8 +576,6 @@ public void createWithYaml( final String envName, final String envYaml, final bo throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); - if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) - installApposeFromSource(envsdir + File.separator + envName); } /** @@ -626,8 +624,6 @@ public void create( final String envName, final boolean isForceCreation ) throws if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); - if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) - installApposeFromSource(envsdir + File.separator + envName); } /** @@ -685,8 +681,6 @@ public void create( final String envName, final boolean isForceCreation, final S cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba(cmd.toArray(new String[0])); - if (this.checkDependencyInEnv(envsdir + File.separator + envName, "python")) - installApposeFromSource(envsdir + File.separator + envName); } /** @@ -728,8 +722,6 @@ public void create( final String envName, final boolean isForceCreation, List Date: Mon, 19 Aug 2024 17:44:30 -0500 Subject: [PATCH 075/120] Add helper method to get env dir for a given name --- src/main/java/org/apposed/appose/Mamba.java | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 99fa275..5066d40 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -459,6 +459,10 @@ public String getEnvsDir() { return this.envsdir; } + public String getEnvDir(String envName) { + return getEnvsDir() + File.separator + envName; + } + /** * Returns {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for * Mac/Linux. @@ -517,7 +521,7 @@ public void updateIn( final String envName, final String... args ) throws IOExce { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", this.envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", getEnvDir(envName) ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba(cmd.toArray(new String[0])); @@ -575,7 +579,7 @@ public void createWithYaml( final String envName, final String envYaml, final bo if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", - envsdir + File.separator + envName, "-f", envYaml, "-y", "-vv" ); + getEnvDir(envName), "-f", envYaml, "-y", "-vv" ); } /** @@ -623,7 +627,7 @@ public void create( final String envName, final boolean isForceCreation ) throws if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - runMamba( "create", "-y", "-p", envsdir + File.separator + envName ); + runMamba( "create", "-y", "-p", getEnvDir(envName) ); } /** @@ -677,7 +681,7 @@ public void create( final String envName, final boolean isForceCreation, final S if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba(cmd.toArray(new String[0])); @@ -715,7 +719,7 @@ public void create( final String envName, final boolean isForceCreation, List cmd = new ArrayList<>( Arrays.asList( "create", "-p", envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); if (channels == null) channels = new ArrayList<>(); for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} if (packages == null) packages = new ArrayList<>(); @@ -851,7 +855,7 @@ public void installIn( final String envName, List channels, List checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", this.envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", getEnvDir(envName) ) ); if (channels == null) channels = new ArrayList<>(); for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} if (packages == null) packages = new ArrayList<>(); @@ -880,7 +884,7 @@ public void installIn( final String envName, final String... args ) throws IOExc { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", this.envsdir + File.separator + envName ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", getEnvDir(envName) ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba(cmd.toArray(new String[0])); @@ -992,9 +996,9 @@ public void runPythonIn( final String envName, final String... args ) throws IOE if (new File(envName, PYTHON_COMMAND).isFile()) { argsList.add( coverArgWithDoubleQuotes(Paths.get( envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); envDir = Paths.get( envName ).toAbsolutePath().toString(); - } else if (Paths.get( this.envsdir, envName, PYTHON_COMMAND ).toFile().isFile()) { - argsList.add( coverArgWithDoubleQuotes(Paths.get( this.envsdir, envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); - envDir = Paths.get( envsdir, envName ).toAbsolutePath().toString(); + } else if (Paths.get( getEnvDir(envName), PYTHON_COMMAND ).toFile().isFile()) { + argsList.add( coverArgWithDoubleQuotes(Paths.get( getEnvDir(envName), PYTHON_COMMAND ).toAbsolutePath().toString()) ); + envDir = Paths.get( getEnvDir(envName) ).toAbsolutePath().toString(); } else throw new IOException("The environment provided (" + envName + ") does not exist or does not contain a Python executable (" + PYTHON_COMMAND + ")."); @@ -1671,6 +1675,6 @@ public static void main(String[] args) throws IOException, InterruptedException, Mamba m = new Mamba("C:\\Users\\angel\\Desktop\\Fiji app\\appose_x86_64"); String envName = "efficientvit_sam_env"; m.pipInstallIn(envName, - m.getEnvsDir() + File.separator + envName + File.separator + "appose-python"); + m.getEnvDir(envName) + File.separator + "appose-python"); } } From e106e2d6dfb5398ae42df9f030f36419e1da9a98 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 19 Aug 2024 17:47:25 -0500 Subject: [PATCH 076/120] Add helper method to get env name from YAML file --- src/main/java/org/apposed/appose/Mamba.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/Mamba.java index 5066d40..be7fe61 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/Mamba.java @@ -60,6 +60,7 @@ package org.apposed.appose; import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.utils.FileNameUtils; import org.apache.commons.lang3.SystemUtils; import com.sun.jna.Platform; @@ -455,6 +456,15 @@ public void installMicromamba() throws IOException, InterruptedException, Archiv checkMambaInstalled(); } + public static String envNameFromYaml(File condaEnvironmentYaml) throws IOException { + List lines = Files.readAllLines(condaEnvironmentYaml.toPath()); + return lines.stream() + .filter(line -> line.startsWith("name:")) + .map(line -> line.substring(5).trim()) + .findFirst() + .orElseGet(() -> FileNameUtils.getBaseName(condaEnvironmentYaml.toPath())); + } + public String getEnvsDir() { return this.envsdir; } From 146dd694cdc1fa403e72d29192c0c31e3b72315f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 19 Aug 2024 17:47:43 -0500 Subject: [PATCH 077/120] Fix NPE when building conda env if baseDir unset --- src/main/java/org/apposed/appose/Builder.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index c59128a..c6cfcf6 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -42,29 +42,31 @@ public class Builder { } public Environment build() { - String base = baseDir.getPath(); - boolean useSystemPath = systemPath; - // TODO Build the thing!~ // Hash the state to make a base directory name. // - Construct conda environment from condaEnvironmentYaml. // - Download and unpack JVM of the given vendor+version. // - Populate ${baseDirectory}/jars with Maven artifacts? + final String base; if (condaEnvironmentYaml != null) { try { Mamba conda = new Mamba(Mamba.BASE_PATH); conda.installMicromamba(); - String envName = "appose"; + String envName = Mamba.envNameFromYaml(condaEnvironmentYaml); if (conda.getEnvironmentNames().contains(envName)) { // TODO: Should we update it? For now, we just use it. } else { conda.createWithYaml(envName, condaEnvironmentYaml.getAbsolutePath()); } + base = conda.getEnvDir(envName); + // TODO: If baseDir is already set, we should use that directory instead. } catch (IOException | InterruptedException | ArchiveException | URISyntaxException | MambaInstallException e) { throw new RuntimeException(e); } } + else base = baseDir.getPath(); + final boolean useSystemPath = systemPath; return new Environment() { @Override public String base() { return base; } From 05a02c0b077b99a7d2be3b6ece96858b529179b2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 20 Aug 2024 19:24:59 -0500 Subject: [PATCH 078/120] Move mamba logic into subpackage --- src/main/java/org/apposed/appose/Builder.java | 2 ++ .../org/apposed/appose/{ => mamba}/CondaException.java | 2 +- .../org/apposed/appose/{ => mamba}/FileDownloader.java | 2 +- src/main/java/org/apposed/appose/{ => mamba}/Mamba.java | 7 ++----- .../apposed/appose/{ => mamba}/MambaInstallException.java | 2 +- .../apposed/appose/{ => mamba}/MambaInstallerUtils.java | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) rename src/main/java/org/apposed/appose/{ => mamba}/CondaException.java (99%) rename src/main/java/org/apposed/appose/{ => mamba}/FileDownloader.java (98%) rename src/main/java/org/apposed/appose/{ => mamba}/Mamba.java (99%) rename src/main/java/org/apposed/appose/{ => mamba}/MambaInstallException.java (98%) rename src/main/java/org/apposed/appose/{ => mamba}/MambaInstallerUtils.java (99%) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index c6cfcf6..5730a47 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -34,6 +34,8 @@ import java.net.URISyntaxException; import org.apache.commons.compress.archivers.ArchiveException; +import org.apposed.appose.mamba.Mamba; +import org.apposed.appose.mamba.MambaInstallException; public class Builder { diff --git a/src/main/java/org/apposed/appose/CondaException.java b/src/main/java/org/apposed/appose/mamba/CondaException.java similarity index 99% rename from src/main/java/org/apposed/appose/CondaException.java rename to src/main/java/org/apposed/appose/mamba/CondaException.java index 2efc7b3..74fe23f 100644 --- a/src/main/java/org/apposed/appose/CondaException.java +++ b/src/main/java/org/apposed/appose/mamba/CondaException.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.apposed.appose; +package org.apposed.appose.mamba; public class CondaException { diff --git a/src/main/java/org/apposed/appose/FileDownloader.java b/src/main/java/org/apposed/appose/mamba/FileDownloader.java similarity index 98% rename from src/main/java/org/apposed/appose/FileDownloader.java rename to src/main/java/org/apposed/appose/mamba/FileDownloader.java index bcb0f5a..b636726 100644 --- a/src/main/java/org/apposed/appose/FileDownloader.java +++ b/src/main/java/org/apposed/appose/mamba/FileDownloader.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.apposed.appose; +package org.apposed.appose.mamba; import java.io.FileOutputStream; import java.io.IOException; diff --git a/src/main/java/org/apposed/appose/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java similarity index 99% rename from src/main/java/org/apposed/appose/Mamba.java rename to src/main/java/org/apposed/appose/mamba/Mamba.java index be7fe61..cdf587c 100644 --- a/src/main/java/org/apposed/appose/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -57,7 +57,7 @@ * POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ -package org.apposed.appose; +package org.apposed.appose.mamba; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.utils.FileNameUtils; @@ -65,7 +65,7 @@ import com.sun.jna.Platform; -import org.apposed.appose.CondaException.EnvironmentExistsException; +import org.apposed.appose.mamba.CondaException.EnvironmentExistsException; import java.io.BufferedReader; import java.io.File; @@ -73,7 +73,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.Channels; @@ -91,8 +90,6 @@ import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; /** * Python environment manager, implemented by delegating to micromamba. diff --git a/src/main/java/org/apposed/appose/MambaInstallException.java b/src/main/java/org/apposed/appose/mamba/MambaInstallException.java similarity index 98% rename from src/main/java/org/apposed/appose/MambaInstallException.java rename to src/main/java/org/apposed/appose/mamba/MambaInstallException.java index efbd5d6..d0dc3f8 100644 --- a/src/main/java/org/apposed/appose/MambaInstallException.java +++ b/src/main/java/org/apposed/appose/mamba/MambaInstallException.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.apposed.appose; +package org.apposed.appose.mamba; /** * Exception to be thrown when Micromamba is not found in the wanted directory diff --git a/src/main/java/org/apposed/appose/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java similarity index 99% rename from src/main/java/org/apposed/appose/MambaInstallerUtils.java rename to src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java index 3b7949e..6a90b23 100644 --- a/src/main/java/org/apposed/appose/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java @@ -27,7 +27,7 @@ * #L% */ -package org.apposed.appose; +package org.apposed.appose.mamba; import java.io.BufferedInputStream; import java.io.File; From 0c57e9848d91db58a50f5e4c713005c1da526ee5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 21 Aug 2024 17:04:42 -0500 Subject: [PATCH 079/120] Revise builder notes to address design wrinkles --- notes.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/notes.md b/notes.md index cbb3b98..07c0ebf 100644 --- a/notes.md +++ b/notes.md @@ -29,7 +29,7 @@ The interface is `BuildHandler`: And the implementations + supported schemes are: * `PixiHandler` -- `environment.yml`, `pixi.toml`, `pypi`, `conda`, and null. * `MavenHandler` -- `maven` -* `OpenJDKHandler` -- `openjdk` +* `JDKHandler` -- `openjdk` Although the term "scheme" might be confused with URI scheme, other terms are problematic too: * "platform" will be confused with OS/arch. @@ -73,6 +73,63 @@ Appose.env() .build() // Whew! ``` +### 2024-08-20 update + +One tricky thing is the base directory when combining paradigms: +* Get rid of "base directory" naming (in conda, the "base" environment is something else, so it's a confusing word here) in favor of `${appose-cache-dir}/${env-name}` convention. By default, `appose-cache-dir` equals `~/.local/share/appose`, but we could provide a way to override it... +* Similarly, get rid of the `base(...)` builder method in favor of adding a `build(String envName) -> Environment` signature. +* But what to name `Environment#base` property now? I really like `base`... Maybe `basedir`? Or `prefix``? +* Each `BuildHandler` catalogs the `include` and `channel` calls it feels are relevant, but does not do anything until `build` is finally called. +* If `build()` is called with no args, then the build handlers are queried sequentially (`default String envName() { return null; }`?). The first non-null name that comes back is taken as truth and then `build(thatName)` is passed to all handlers. Otherwise, an exception is raised "No environment name given". +* Environments all live in `~/.local/share/appose/`, where `` is the name of the environment. If `name:` is given in a `pixi.toml` or `environment.yml`, great, the `PixiBuildHandler` can parse out that string when its `envName()` method is called. + +What about starting child processes via `pixi run`? That's not agnostic of the builder... +* What if the build handlers are also involved in child process launches? The `service(exes, args)` method could delegate to them... +* `BuildHandler` → `EnvHandler`? +* In `Environment`, how about replacing `use_system_path` with just a `path` list of dirs to check for executables being launched by `service`? Then we could dispense with the boilerplate `python`, `bin/python` repetition. +* Each env handler gets a chance to influence the worker launch args... and/or bin dirs... + - The pixi handler could prepend `.../pixi run` when a `pixi.toml` is present. + But this is tricky, because it goes *before* the selected exe... pre-args vs post-args uhhh + +So, environment has: +* path (list of directories -- only used if `all_args[0]` is not already an absolute path to an executable program already?) +* launcher (list of string args to prepend) +* classpath (list of elements to include when running java) + +* `Map>` is what's returned by the `build(...)` step of each `BuildHandler`. + - Relevant keys include: "path", "classpath", "launcher" + - The `new Environment() { ... }` invocation will aggregate the values given here into its accessors. + +* When running a service, it first uses the path to search for the requested exe, before prepending the launcher args. + - What about pixi + cjdk? We'll need the full path to java... + - How can we tell the difference between that and pixi alone with openjdk from conda-forge? + - In the former case, we need the full path, in the latter case, no. + - Pixi should just put `${envDir}/bin` onto the path, no? + - There is an edge case where `pixi run foo` works, even though `foo` is not an executable on the path... in which case, the environment service can just plow ahead with it when it can't find a `foo` executable on the path. But it should *first* make an attempt to reify the `foo` from the environment `path` before punting in that way. + +#### pixi + +During its build step, it prepends `[/path/to/pixi, run]` to the environment's launcher list, and `${env-dir}/bin` to the environment's path list. + +#### cjdk + +``` +cjdk -j adoptium:21 java-home +/home/curtis/.cache/cjdk/v0/jdks/d217ee819493b9c56beed2e4d481e4c370de993d/jdk-21.0.4+7 +/home/curtis/.cache/cjdk/v0/jdks/d217ee819493b9c56beed2e4d481e4c370de993d/jdk-21.0.4+7/bin/java -version +openjdk version "21.0.4" 2024-07-16 LTS +OpenJDK Runtime Environment Temurin-21.0.4+7 (build 21.0.4+7-LTS) +OpenJDK 64-Bit Server VM Temurin-21.0.4+7 (build 21.0.4+7-LTS, mixed mode, sharing) +``` + +So `JDKHandler`, during its build step, prepends the java-home directory to the environment's path: `$(cjdk -j adoptium:21 java-home)/bin` + +#### Maven + +No need to add any directories to the environment path! + +However, if Maven artifacts are added via `includes`, they should not only be downloaded, but also be part of the class path when launching java-based programs. We could do that by putting classpath into the `Environment` class directly, along side `path`... it's only a little hacky ;_; + ## Pixi Is even better than micromamba. It's a great fit for Appose's requirements. @@ -174,7 +231,7 @@ So we'll just settle for pixi's standard behavior here: a single environment named `default` per pixi project, with one pixi project per Appose environment. Unfortunately, that means our environment directory structure will be: ``` -~/.local/share/appose/sc-fiji-spiff/envs/default +~/.local/share/appose/sc-fiji-spiff/.pixi/envs/default ``` for an Appose environment named `sc-fiji-spiff`. (Note that environment names cannot contain dots, only alphameric and dash.) @@ -197,7 +254,7 @@ So there is no escape from pixi's directory convention of: ``` Which of these is the least annoying? ``` -~/.local/share/appose/sc-fiji-spiff/envs/default +~/.local/share/appose/sc-fiji-spiff/.pixi/envs/default ~/.local/share/appose/sc-fiji-spiff-782634298734/envs/default ~/.local/share/appose/sc-fiji-spiff/envs/sc-fiji-spiff ~/.local/share/appose/sc-fiji-spiff-782634298734/envs/sc-fiji-spiff @@ -209,7 +266,7 @@ behavior anyway. The only shorter one would be: ``` -~/.local/share/appose/envs/sc-fiji-spiff +~/.local/share/appose/.pixi/envs/sc-fiji-spiff ``` if we opted to keep `~/.local/share/appose` as a single Pixi project root with multiple environments... but the inconvenience and risks From 7fee9af1b4e6d672eae3be362aa1773bda35690b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Aug 2024 15:22:16 -0500 Subject: [PATCH 080/120] Builder: remove unused imports --- src/main/java/org/apposed/appose/Builder.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 5730a47..d8185ef 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -33,10 +33,6 @@ import java.io.IOException; import java.net.URISyntaxException; -import org.apache.commons.compress.archivers.ArchiveException; -import org.apposed.appose.mamba.Mamba; -import org.apposed.appose.mamba.MambaInstallException; - public class Builder { Builder() { From ba75fa04c1e685851194d12f4f0febe6a6a2eae4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Aug 2024 15:22:44 -0500 Subject: [PATCH 081/120] Encapsulate ArchiveException from unTar failures IOException is close enough. --- src/main/java/org/apposed/appose/Builder.java | 2 +- src/main/java/org/apposed/appose/mamba/Mamba.java | 6 ++---- .../org/apposed/appose/mamba/MambaInstallerUtils.java | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index d8185ef..1131ba9 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -59,7 +59,7 @@ public Environment build() { } base = conda.getEnvDir(envName); // TODO: If baseDir is already set, we should use that directory instead. - } catch (IOException | InterruptedException | ArchiveException | URISyntaxException | MambaInstallException e) { + } catch (IOException | InterruptedException | URISyntaxException | MambaInstallException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index cdf587c..5610df7 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -59,7 +59,6 @@ package org.apposed.appose.mamba; -import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.utils.FileNameUtils; import org.apache.commons.lang3.SystemUtils; @@ -418,7 +417,7 @@ private File downloadMicromamba() throws IOException, URISyntaxException { } private void decompressMicromamba(final File tempFile) - throws IOException, ArchiveException, InterruptedException { + throws IOException, InterruptedException { final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); @@ -443,10 +442,9 @@ private void decompressMicromamba(final File tempFile) * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws ArchiveException if there is any error decompressing * @throws URISyntaxException if there is any error with the micromamba url */ - public void installMicromamba() throws IOException, InterruptedException, ArchiveException, URISyntaxException { + public void installMicromamba() throws IOException, InterruptedException, URISyntaxException { checkMambaInstalled(); if (installed) return; decompressMicromamba(downloadMicromamba()); diff --git a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java index 6a90b23..1e0acdb 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java @@ -116,9 +116,8 @@ private static long copy(final InputStream input, final OutputStream output) thr * @param outputDir the output directory file. * @throws IOException * @throws FileNotFoundException - * @throws ArchiveException */ - public static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, ArchiveException, InterruptedException { + public static void unTar(final File inputFile, final File outputDir) throws FileNotFoundException, IOException, InterruptedException { try ( InputStream is = new FileInputStream(inputFile); @@ -143,7 +142,9 @@ public static void unTar(final File inputFile, final File outputDir) throws File } } } - } + } catch (ArchiveException e) { + throw new IOException(e); + } } @@ -153,11 +154,10 @@ public static void unTar(final File inputFile, final File outputDir) throws File * no args are required * @throws FileNotFoundException if some file is not found * @throws IOException if there is any error reading or writting - * @throws ArchiveException if there is any error decompressing * @throws URISyntaxException if the url is wrong or there is no internet connection * @throws InterruptedException if there is interrruption */ - public static void main(String[] args) throws FileNotFoundException, IOException, ArchiveException, URISyntaxException, InterruptedException { + public static void main(String[] args) throws FileNotFoundException, IOException, URISyntaxException, InterruptedException { String url = Mamba.MICROMAMBA_URL; final File tempFile = File.createTempFile( "miniconda", ".tar.bz2" ); tempFile.deleteOnExit(); From 73b1518cbedd774cdb7a3733c8b1734cff593b26 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Aug 2024 15:23:15 -0500 Subject: [PATCH 082/120] Add TODO to fix the user agent It should use the current version string of Appose, not hardcoded 0.1.0. And it's missing a terminating right paren. --- src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java index 1e0acdb..64353bb 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java @@ -231,6 +231,7 @@ public static long getFileSize(URL url) { HttpURLConnection conn = null; try { conn = (HttpURLConnection) url.openConnection(); + // TODO: Fix user agent. conn.setRequestProperty("User-Agent", "Appose/0.1.0(" + System.getProperty("os.name") + "; Java " + System.getProperty("java.version")); if (conn.getResponseCode() >= 300 && conn.getResponseCode() <= 308) return getFileSize(redirectedURL(url)); From f8b8248186074d3bedd73851014191a195a1e8ad Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 26 Aug 2024 22:18:39 -0500 Subject: [PATCH 083/120] Add FilePaths methods to merge directories It's tricky when opinionated build handlers want total control of the environment directory. With this functionality, we can give each handler what it wants, and then merge everything back together after the fact. --- .../java/org/apposed/appose/FilePaths.java | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/FilePaths.java b/src/main/java/org/apposed/appose/FilePaths.java index 1a3d2ee..876db7e 100644 --- a/src/main/java/org/apposed/appose/FilePaths.java +++ b/src/main/java/org/apposed/appose/FilePaths.java @@ -30,12 +30,16 @@ package org.apposed.appose; import java.io.File; +import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; /** - * Utility methods for working with file paths. + * Utility methods for working with files. */ public final class FilePaths { @@ -78,4 +82,112 @@ public static File findExe(List dirs, List exes) { } return null; } + + /** + * Merges the files of the given source directory into the specified destination directory. + *

+ * For example, {@code moveDirectory(foo, bar)} would move: + *

+ *
    + *
  • {@code foo/a.txt} → {@code bar/a.txt}
  • + *
  • {@code foo/b.dat} → {@code bar/b.dat}
  • + *
  • {@code foo/c.csv} → {@code bar/c.csv}
  • + *
  • {@code foo/subfoo/d.doc} → {@code bar/subfoo/d.doc}
  • + *
  • etc.
  • + *
+ * + * @param srcDir TODO + * @param destDir TODO + * @param overwrite TODO + */ + public static void moveDirectory(File srcDir, File destDir, boolean overwrite) throws IOException { + if (!srcDir.isDirectory()) throw new IllegalArgumentException("Not a directory: " + srcDir); + if (!destDir.isDirectory()) throw new IllegalArgumentException("Not a directory: " + destDir); + try (DirectoryStream stream = Files.newDirectoryStream(srcDir.toPath())) { + for (Path srcPath : stream) moveFile(srcPath.toFile(), destDir, overwrite); + } + if (!srcDir.delete()) throw new IOException("Could not remove directory " + destDir); + } + + /** + * Moves the given source file to the destination directory, + * creating intermediate destination directories as needed. + *

+ * If the destination {@code file.ext} already exists, one of two things will happen: either + * A) the existing destination file will be renamed as a backup to {@code file.ext.old}—or + * {@code file.ext.0.old}, {@code file.ext.1.old}, etc., if {@code file.ext.old} also already exists—or + * B) the source file will be renamed as a backup in this manner. + * Which behavior occurs depends on the value of the {@code overwrite} flag: + * true to back up the destination file, or false to back up the source file. + *

+ * + * @param srcFile Source file to move. + * @param destDir Destination directory into which the file will be moved. + * @param overwrite If true, "overwrite" the destination file with the source file, + * backing up any existing destination file first; if false, + * leave the original destination file in place, instead moving + * the source file to a backup destination as a "previous" version. + * @throws IOException If something goes wrong with the needed I/O operations. + */ + public static void moveFile(File srcFile, File destDir, boolean overwrite) throws IOException { + File destFile = new File(destDir, srcFile.getName()); + if (srcFile.isDirectory()) { + // Create matching destination directory as needed. + if (!destFile.exists() && !destFile.mkdirs()) + throw new IOException("Failed to create destination directory: " + destDir); + // Recurse over source directory contents. + moveDirectory(srcFile, destFile, overwrite); + return; + } + // Source file is not a directory; move it into the destination directory. + if (destDir.exists() && !destDir.isDirectory()) throw new IllegalArgumentException("Non-directory destination path: " + destDir); + if (!destDir.exists() && !destDir.mkdirs()) throw new IOException("Failed to create destination directory: " + destDir); + if (destFile.exists() && !overwrite) { + // Destination already exists, and we aren't allowed to rename it. So we instead + // rename the source file directly to a backup filename in the destination directory. + renameToBackup(srcFile, destDir); + return; + } + + // Rename the existing destination file (if any) to a + // backup file, then move the source file into place. + renameToBackup(destFile); + if (!srcFile.renameTo(destFile)) throw new IOException("Failed to move file: " + srcFile + " -> " + destFile); + } + + /** + * TODO + * + * @param srcFile TODO + * @throws IOException If something goes wrong with the needed I/O operations. + */ + public static void renameToBackup(File srcFile) throws IOException { + renameToBackup(srcFile, srcFile.getParentFile()); + } + + /** + * TODO + * + * @param srcFile TODO + * @param destDir TODO + * @throws IOException If something goes wrong with the needed I/O operations. + */ + public static void renameToBackup(File srcFile, File destDir) throws IOException { + if (!srcFile.exists()) return; // Nothing to back up! + String prefix = srcFile.getName(); + String suffix = "old"; + File backupFile = new File(destDir, prefix + "." + suffix); + for (int i = 0; i < 1000; i++) { + if (!backupFile.exists()) break; + // The .old backup file already exists! Try .0.old, .1.old, and so on. + backupFile = new File(destDir, prefix + "." + i + "." + suffix); + } + if (backupFile.exists()) { + File failedTarget = new File(destDir, prefix + "." + suffix); + throw new UnsupportedOperationException("Too many backup files already exist for target: " + failedTarget); + } + if (!srcFile.renameTo(backupFile)) { + throw new IOException("Failed to rename file:" + srcFile + " -> " + backupFile); + } + } } From c5e0db2fe0ef3c60566f31011b9460d499b3d746 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 26 Aug 2024 14:31:29 -0500 Subject: [PATCH 084/120] Test the FilePaths utility methods --- .../org/apposed/appose/FilePathsTest.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/test/java/org/apposed/appose/FilePathsTest.java diff --git a/src/test/java/org/apposed/appose/FilePathsTest.java b/src/test/java/org/apposed/appose/FilePathsTest.java new file mode 100644 index 0000000..24396fa --- /dev/null +++ b/src/test/java/org/apposed/appose/FilePathsTest.java @@ -0,0 +1,195 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.apposed.appose; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests {@link FilePaths}. + * + * @author Curtis Rueden + */ +public class FilePathsTest { + + /** Tests {@link FilePaths#findExe}. */ + @Test + public void testFindExe() throws IOException { + File tmpDir = Files.createTempDirectory("appose-FilePathsTest-testFindExe-").toFile(); + try { + // Set up some red herrings. + File walk = createStubFile(tmpDir, "walk"); + File fly = createStubFile(tmpDir, "fly"); + File binDir = createDirectory(tmpDir, "bin"); + File binFly = createStubFile(binDir, "fly"); + // Mark the desired match as executable. + assertTrue(binFly.setExecutable(true)); + assertTrue(binFly.canExecute()); + + // Search for the desired match. + List dirs = Arrays.asList(tmpDir.getAbsolutePath(), binDir.getAbsolutePath()); + List exes = Arrays.asList("walk", "fly", "swim"); + File exe = FilePaths.findExe(dirs, exes); + + // Check that we found the right file. + assertEquals(binFly, exe); + } + finally { + FileUtils.deleteDirectory(tmpDir); + } + } + + /** Tests {@link FilePaths#location}. */ + @Test + public void testLocation() { + // NB: Will fail if this test is run in a weird way (e.g. + // from inside the tests JAR), but I don't care right now. :-P + File expected = Paths.get(System.getProperty("user.dir"), "target", "test-classes").toFile(); + File actual = FilePaths.location(getClass()); + assertEquals(expected, actual); + } + + /** Tests {@link FilePaths#moveDirectory}. */ + @Test + public void testMoveDirectory() throws IOException { + File tmpDir = Files.createTempDirectory("appose-FilePathsTest-testMoveDirectory-").toFile(); + try { + // Set up a decently weighty directory structure. + File srcDir = createDirectory(tmpDir, "src"); + File breakfast = createStubFile(srcDir, "breakfast"); + File lunchDir = createDirectory(srcDir, "lunch"); + File lunchFile1 = createStubFile(lunchDir, "apples", "fuji"); + File lunchFile2 = createStubFile(lunchDir, "bananas"); + File dinnerDir = createDirectory(srcDir, "dinner"); + File dinnerFile1 = createStubFile(dinnerDir, "bread"); + File dinnerFile2 = createStubFile(dinnerDir, "wine"); + File destDir = createDirectory(tmpDir, "dest"); + File destLunchDir = createDirectory(destDir, "lunch"); + File destLunchFile1 = createStubFile(destLunchDir, "apples", "gala"); + + // Move the source directory to the destination. + FilePaths.moveDirectory(srcDir, destDir, false); + + // Check whether everything worked. + assertFalse(srcDir.exists()); + assertMoved(breakfast, destDir, ""); + assertMoved(lunchFile1, destLunchDir, "gala"); + File backupLunchFile1 = new File(destLunchDir, "apples.old"); + assertContent(backupLunchFile1, "fuji"); + assertMoved(lunchFile2, destLunchDir, ""); + File destDinnerDir = new File(destDir, dinnerDir.getName()); + assertMoved(dinnerFile1, destDinnerDir, ""); + assertMoved(dinnerFile2, destDinnerDir, ""); + } + finally { + FileUtils.deleteDirectory(tmpDir); + } + } + + /** Tests {@link FilePaths#moveFile}. */ + @Test + public void testMoveFile() throws IOException { + File tmpDir = Files.createTempDirectory("appose-FilePathsTest-testMoveFile-").toFile(); + try { + File srcDir = createDirectory(tmpDir, "from"); + File srcFile = createStubFile(srcDir, "stuff.txt", "shiny"); + File destDir = createDirectory(tmpDir, "to"); + File destFile = createStubFile(destDir, "stuff.txt", "obsolete"); + boolean overwrite = true; + + FilePaths.moveFile(srcFile, destDir, overwrite); + + assertTrue(srcDir.exists()); + assertFalse(srcFile.exists()); + assertContent(destFile, "shiny"); + File backupFile = new File(destDir, "stuff.txt.old"); + assertContent(backupFile, "obsolete"); + } + finally { + FileUtils.deleteDirectory(tmpDir); + } + } + + /** Tests {@link FilePaths#renameToBackup}. */ + @Test + public void testRenameToBackup() throws IOException { + File tmpFile = Files.createTempFile("appose-FilePathsTest-testRenameToBackup-", "").toFile(); + assertTrue(tmpFile.exists()); + tmpFile.deleteOnExit(); + FilePaths.renameToBackup(tmpFile); + File backupFile = new File(tmpFile.getParent(), tmpFile.getName() + ".old"); + backupFile.deleteOnExit(); + assertFalse(tmpFile.exists()); + assertTrue(backupFile.exists()); + } + + private File createDirectory(File parent, String name) { + File dir = new File(parent, name); + assertTrue(dir.mkdir()); + assertTrue(dir.exists()); + return dir; + } + + private File createStubFile(File dir, String name) throws IOException { + return createStubFile(dir, name, "<" + name + ">"); + } + + private File createStubFile(File dir, String name, String content) throws IOException { + File stubFile = new File(dir, name); + try (PrintWriter pw = new PrintWriter(new FileWriter(stubFile))) { + pw.print(content); + } + assertTrue(stubFile.exists()); + return stubFile; + } + + private void assertMoved(File srcFile, File destDir, String expectedContent) throws IOException { + assertFalse(srcFile.exists()); + File destFile = new File(destDir, srcFile.getName()); + assertContent(destFile, expectedContent); + } + + private void assertContent(File file, String expectedContent) throws IOException { + assertTrue(file.exists()); + String actualContent = new String(Files.readAllBytes(file.toPath())); + assertEquals(expectedContent, actualContent); + } +} From 18a313e7c5ed4477c86ff3267d50d5cca9361a81 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 26 Aug 2024 22:21:07 -0500 Subject: [PATCH 085/120] Add BuildHandler interface --- .../java/org/apposed/appose/BuildHandler.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/org/apposed/appose/BuildHandler.java diff --git a/src/main/java/org/apposed/appose/BuildHandler.java b/src/main/java/org/apposed/appose/BuildHandler.java new file mode 100644 index 0000000..7315f53 --- /dev/null +++ b/src/main/java/org/apposed/appose/BuildHandler.java @@ -0,0 +1,72 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.apposed.appose; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface BuildHandler { + + /** + * Registers a channel from which elements of the environment can be obtained. + * + * @param name The name of the channel to register. + * @param location The location of the channel (e.g. a URI), or {@code null} if the + * name alone is sufficient to unambiguously identify the channel. + * @return true iff the channel is understood by this build handler implementation. + * @see Builder#channel + */ + boolean channel(String name, String location); + + /** + * Registers content to be included within the environment. + * + * @param content The content to include in the environment, fetching if needed. + * @param scheme The type of content, which serves as a hint for + * how to interpret the content in some scenarios. + * @see Builder#include + */ + boolean include(String content, String scheme); + + /** Suggests a name for the environment currently being built. */ + String envName(); + + /** + * Executes the environment build, according to the configured channels and includes. + * + * @param envDir The directory into which the environment will be built. + * @param config The table into which environment configuration will be recorded. + * @see Builder#build(String) + * @throws IOException If something goes wrong building the environment. + */ + void build(File envDir, Map> config) throws IOException; +} From c3dcf0d455290b7d869675812a24fd06354691fb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 27 Aug 2024 07:59:55 -0500 Subject: [PATCH 086/120] Fix Mamba class authors --- src/main/java/org/apposed/appose/mamba/Mamba.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 5610df7..73c81e2 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -94,7 +94,7 @@ * Python environment manager, implemented by delegating to micromamba. * * @author Ko Sugawara - * @author Curtis Rueden + * @author Carlos Garcia */ public class Mamba { From 391ac2d6ae91cdeda9d569a6ec185c769169918a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 27 Aug 2024 08:00:08 -0500 Subject: [PATCH 087/120] Replace non-breaking spaces with regular spaces --- .../java/org/apposed/appose/mamba/Mamba.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 73c81e2..85cdf42 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -112,11 +112,11 @@ public class Mamba { *
 	 * rootdir
 	 * ├── bin
-	 * │   ├── micromamba(.exe)
-	 * │   ... 
+	 * │   ├── micromamba(.exe)
+	 * │   ...
 	 * ├── envs
-	 * │   ├── your_env
-	 * │   │   ├── python(.exe)
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
 	 * 
*/ private final String rootdir; @@ -276,11 +276,11 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) *
 	 * MAMBA_ROOT
 	 * ├── bin
-	 * │   ├── micromamba(.exe)
-	 * │   ... 
+	 * │   ├── micromamba(.exe)
+	 * │   ...
 	 * ├── envs
-	 * │   ├── your_env
-	 * │   │   ├── python(.exe)
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
 	 * 
*/ public Mamba() { @@ -298,11 +298,11 @@ public Mamba() { *
 	 * MAMBA_ROOT
 	 * ├── bin
-	 * │   ├── micromamba(.exe)
-	 * │   ... 
+	 * │   ├── micromamba(.exe)
+	 * │   ...
 	 * ├── envs
-	 * │   ├── your_env
-	 * │   │   ├── python(.exe)
+	 * │   ├── your_env
+	 * │   │   ├── python(.exe)
 	 * 
* * @param rootdir From 48885672ea10c2bece91f2c97c92665fa449ed8e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 27 Aug 2024 08:01:59 -0500 Subject: [PATCH 088/120] Make copyright notice not be a javadoc --- src/main/java/org/apposed/appose/mamba/Mamba.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 85cdf42..f97e020 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -30,7 +30,7 @@ // Adapted from JavaConda (https://github.com/javaconda/javaconda), // which has the following license: -/******************************************************************************* +/*-***************************************************************************** * Copyright (C) 2021, Ko Sugawara * All rights reserved. * @@ -55,7 +55,7 @@ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ + ****************************************************************************-*/ package org.apposed.appose.mamba; From e6b238299b08547bd11bc19cb9596c2fff91b7b0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 12:56:10 -0500 Subject: [PATCH 089/120] Migrate Mamba builder to a BuildHandler plugin --- src/main/java/org/apposed/appose/Appose.java | 58 +++- src/main/java/org/apposed/appose/Builder.java | 317 +++++++++++++++--- .../java/org/apposed/appose/Environment.java | 34 +- .../java/org/apposed/appose/mamba/Mamba.java | 24 +- .../apposed/appose/mamba/MambaHandler.java | 203 +++++++++++ .../services/org.apposed.appose.BuildHandler | 1 + .../java/org/apposed/appose/ApposeTest.java | 4 +- .../apposed/appose/NDArrayExamplePython.java | 2 +- 8 files changed, 552 insertions(+), 91 deletions(-) create mode 100644 src/main/java/org/apposed/appose/mamba/MambaHandler.java create mode 100644 src/main/resources/META-INF/services/org.apposed.appose.BuildHandler diff --git a/src/main/java/org/apposed/appose/Appose.java b/src/main/java/org/apposed/appose/Appose.java index b12be19..a84d5f6 100644 --- a/src/main/java/org/apposed/appose/Appose.java +++ b/src/main/java/org/apposed/appose/Appose.java @@ -30,6 +30,7 @@ package org.apposed.appose; import java.io.File; +import java.io.IOException; /** * Appose is a library for interprocess cooperation with shared memory. The @@ -152,27 +153,64 @@ */ public class Appose { - public static Builder base(File directory) { - return new Builder().base(directory); + public static Builder scheme(String scheme) { + return new Builder().scheme(scheme); } - public static Builder base(String directory) { - return base(new File(directory)); + public static Builder file(String filePath) throws IOException { + return new Builder().file(filePath); } - public static Builder conda(File environmentYaml) { - return new Builder().conda(environmentYaml); + public static Builder file(String filePath, String scheme) throws IOException { + return new Builder().file(filePath, scheme); } - public static Environment system() { + public static Builder file(File file) throws IOException { + return new Builder().file(file); + } + + public static Builder file(File file, String scheme) throws IOException { + return new Builder().file(file, scheme); + } + + public static Builder channel(String name) { + return new Builder().channel(name); + } + + public static Builder channel(String name, String location) { + return new Builder().channel(name, location); + } + + public static Builder include(String content) { + return new Builder().include(content); + } + + public static Builder include(String content, String scheme) { + return new Builder().include(content, scheme); + } + + @Deprecated + public static Builder conda(File environmentYaml) throws IOException { + return file(environmentYaml, "environment.yml"); + } + + public static Environment build(File directory) throws IOException { + return new Builder().build(directory); + } + + public static Environment build(String directory) throws IOException { + return build(new File(directory)); + } + + public static Environment system() throws IOException { return system(new File(".")); } - public static Environment system(File directory) { - return new Builder().base(directory).useSystemPath().build(); + public static Environment system(File directory) throws IOException { + return new Builder().useSystemPath().build(directory); } - public static Environment system(String directory) { + public static Environment system(String directory) throws IOException { return system(new File(directory)); } } diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 1131ba9..b10958f 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -31,69 +31,288 @@ import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.Function; +import java.util.stream.Collectors; +/** + * TODO + * + * @author Curtis Rueden + */ public class Builder { - - Builder() { - // Prevent external instantiation. - } - - public Environment build() { - // TODO Build the thing!~ - // Hash the state to make a base directory name. - // - Construct conda environment from condaEnvironmentYaml. - // - Download and unpack JVM of the given vendor+version. - // - Populate ${baseDirectory}/jars with Maven artifacts? - - final String base; - if (condaEnvironmentYaml != null) { - try { - Mamba conda = new Mamba(Mamba.BASE_PATH); - conda.installMicromamba(); - String envName = Mamba.envNameFromYaml(condaEnvironmentYaml); - if (conda.getEnvironmentNames().contains(envName)) { - // TODO: Should we update it? For now, we just use it. - } else { - conda.createWithYaml(envName, condaEnvironmentYaml.getAbsolutePath()); - } - base = conda.getEnvDir(envName); - // TODO: If baseDir is already set, we should use that directory instead. - } catch (IOException | InterruptedException | URISyntaxException | MambaInstallException e) { - throw new RuntimeException(e); - } - } - else base = baseDir.getPath(); - final boolean useSystemPath = systemPath; - return new Environment() { - @Override public String base() { return base; } - @Override public boolean useSystemPath() { return useSystemPath; } - }; - } + private final List handlers; - // -- Configuration -- + private boolean includeSystemPath; + private String scheme = "conda"; - private boolean systemPath; + Builder() { + handlers = new ArrayList<>(); + ServiceLoader.load(BuildHandler.class).forEach(handlers::add); + } + /** + * TODO + * + * @return This {@code Builder} instance, for fluent-style programming. + */ public Builder useSystemPath() { - systemPath = true; + includeSystemPath = true; return this; } - private File baseDir; - - public Builder base(File directory) { - baseDir = directory; + /** + * Sets the scheme to use with subsequent {@link #channel(String)} and + * {@link #include(String)} directives. + * + * @param scheme TODO + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder scheme(String scheme) { + this.scheme = scheme; return this; } - // -- Conda -- + /** + * TODO + * + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder file(String filePath) throws IOException { + return file(new File(filePath)); + } - private File condaEnvironmentYaml; + /** + * TODO + * + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder file(String filePath, String scheme) throws IOException { + return file(new File(filePath), scheme); + } - public Builder conda(File environmentYaml) { - this.condaEnvironmentYaml = environmentYaml; - return this; + /** + * TODO + * + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder file(File file) throws IOException { + return file(file, file.getName()); + } + + /** + * TODO + * + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder file(File file, String scheme) throws IOException { + byte[] bytes = Files.readAllBytes(file.toPath()); + return include(new String(bytes), scheme); + } + + /** + * Registers a channel that provides components of the environment, + * according to the currently configured scheme ("conda" by default). + *

+ * For example, {@code channel("bioconda")} registers the {@code bioconda} + * channel as a source for conda packages. + *

+ * + * @param name The name of the channel to register. + * @return This {@code Builder} instance, for fluent-style programming. + * @see #channel(String, String) + * @see #scheme(String) + */ + public Builder channel(String name) { + return channel(name, scheme); + } + + /** + * Registers a channel that provides components of the environment. + * How to specify a channel is implementation-dependent. Examples: + * + *
    + *
  • {@code channel("bioconda")} - + * to register the {@code bioconda} channel as a source for conda packages.
  • + *
  • {@code channel("scijava", "maven:https://maven.scijava.org/content/groups/public")} - + * to register the SciJava Maven repository as a source for Maven artifacts.
  • + *
+ * + * @param name The name of the channel to register. + * @param location The location of the channel (e.g. a URI), or {@code null} if the + * name alone is sufficient to unambiguously identify the channel. + * @return This {@code Builder} instance, for fluent-style programming. + * @throws IllegalArgumentException if the channel is not understood by any of the available build handlers. + */ + public Builder channel(String name, String location) { + // Pass the channel directive to all handlers. + if (handle(handler -> handler.channel(name, location))) return this; + // None of the handlers accepted the directive. + throw new IllegalArgumentException("Unsupported channel: " + name + + (location == null ? "" : "=" + location)); + } + + /** + * TODO + * + * @param content TODO + * @return This {@code Builder} instance, for fluent-style programming. + * @see #include(String, String) + * @see #scheme(String) + */ + public Builder include(String content) { + return include(content, scheme); + } + + /** + * Registers content to be included within the environment. + * How to specify the content is implementation-dependent. Examples: + *
    + *
  • {@code include("cowsay", "pypi")} - + * Install {@code cowsay} from the Python package index.
  • + *
  • {@code include("openjdk=17")} - + * Install {@code openjdk} version 17 from conda-forge.
  • + *
  • {@code include("bioconda::sourmash")} - + * Specify a conda channel explicitly using environment.yml syntax.
  • + *
  • {@code include("org.scijava:parsington", "maven")} - + * Install the latest version of Parsington from Maven Central.
  • + *
  • {@code include("org.scijava:parsington:2.0.0", "maven")} - + * Install Parsington 2.0.0 from Maven Central.
  • + *
  • {@code include("sc.fiji:fiji", "maven")} - + * Install the latest version of Fiji from registered Maven repositories.
  • + *
  • {@code include("zulu:17", "jdk")} - + * Install version 17 of Azul Zulu OpenJDK.
  • + *
  • {@code include(yamlString, "environment.yml")} - + * Provide the literal contents of a conda {@code environment.yml} file, + * indicating a set of packages to include. + *
+ *

+ * Note that content is not actually fetched or installed until + * {@link #build} is called at the end of the builder chain. + *

+ * + * @param content The content (e.g. a package name, or perhaps the contents of an environment + * configuration file) to include in the environment, fetching if needed. + * @param scheme The type of content, which serves as a hint for how to interpret + * the content in some scenarios; see above for examples. + * @return This {@code Builder} instance, for fluent-style programming. + * @throws IllegalArgumentException if the include directive is not understood by any of the available build handlers. + */ + public Builder include(String content, String scheme) { + // Pass the include directive to all handlers. + if (handle(handler -> handler.include(content, scheme))) return this; + // None of the handlers accepted the directive. + throw new IllegalArgumentException("Unsupported '" + scheme + "' content: " + content); + } + + /** + * Executes the environment build, according to the configured channels and includes, + * with a name inferred from the configuration registered earlier. For example, if + * {@code environment.yml} content was registered, the name from that configuration will be used. + * + * @return The newly constructed Appose {@link Environment}, + * from which {@link Service}s can be launched. + * @see #build(String) + * @throws IllegalStateException if no name can be inferred from included content. + * @throws IOException If something goes wrong building the environment. + */ + public Environment build() throws IOException { + // Autodetect the environment name from the available build handlers. + return build(handlers.stream() + .map(BuildHandler::envName) + .filter(Objects::nonNull) + .findFirst() + .orElse(null)); + } + + /** + * Executes the environment build, according to the configured channels and includes. + * with a base directory inferred from the given name. + * + * @param envName The name of the environment to build. + * @return The newly constructed Appose {@link Environment}, + * from which {@link Service}s can be launched. + * @throws IOException If something goes wrong building the environment. + */ + public Environment build(String envName) throws IOException { + if (envName == null || envName.isEmpty()) { + throw new IllegalArgumentException("No environment name given."); + } + // TODO: Make Appose's root directory configurable. + Path apposeRoot = Paths.get(System.getProperty("user.home"), ".local", "share", "appose"); + return build(apposeRoot.resolve(envName).toFile()); + } + + /** + * Executes the environment build, according to the configured channels and includes. + * with the given base directory. + * + * @param envDir The directory in which to construct the environment. + * @return The newly constructed Appose {@link Environment}, + * from which {@link Service}s can be launched. + * @throws IOException If something goes wrong building the environment. + */ + public Environment build(File envDir) throws IOException { + if (envDir == null) { + throw new IllegalArgumentException("No environment base directory given."); + } + if (!envDir.exists()) { + if (!envDir.mkdirs()) { + throw new RuntimeException("Failed to create environment base directory: " + envDir); + } + } + if (!envDir.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + envDir); + } + + Map> config = new HashMap<>(); + for (BuildHandler handler : handlers) handler.build(envDir, config); + + String base = envDir.getAbsolutePath(); + + List launchArgs = listFromConfig("launchArgs", config); + List binPaths = listFromConfig("binPaths", config); + List classpath = listFromConfig("classpath", config); + + // Always add environment directory itself to the binPaths. + // Especially important on Windows, where python.exe is not tucked into a bin subdirectory. + binPaths.add(envDir.getAbsolutePath()); + + if (includeSystemPath) { + List systemPaths = Arrays.asList(System.getenv("PATH").split(File.pathSeparator)); + binPaths.addAll(systemPaths); + } + + return new Environment() { + @Override public String base() { return base; } + @Override public List binPaths() { return binPaths; } + @Override public List classpath() { return classpath; } + @Override public List launchArgs() { return launchArgs; } + }; + } + + // -- Helper methods -- + + private boolean handle(Function handlerFunction) { + boolean handled = false; + for (BuildHandler handler : handlers) + handled |= handlerFunction.apply(handler); + return handled; + } + + private static List listFromConfig(String key, Map> config) { + List value = config.getOrDefault(key, Collections.emptyList()); + return value.stream().map(Object::toString).collect(Collectors.toList()); } } diff --git a/src/main/java/org/apposed/appose/Environment.java b/src/main/java/org/apposed/appose/Environment.java index 4891b65..d442a6e 100644 --- a/src/main/java/org/apposed/appose/Environment.java +++ b/src/main/java/org/apposed/appose/Environment.java @@ -40,8 +40,10 @@ public interface Environment { - default String base() { return "."; } - default boolean useSystemPath() { return false; } + String base(); + List binPaths(); + List classpath(); + List launchArgs(); /** * Creates a Python script service. @@ -56,10 +58,7 @@ public interface Environment { * @throws IOException If something goes wrong starting the worker process. */ default Service python() throws IOException { - List pythonExes = Arrays.asList( - "python", "python3", "python.exe", - "bin/python", "bin/python.exe" - ); + List pythonExes = Arrays.asList("python", "python3", "python.exe"); return service(pythonExes, "-c", "import appose.python_worker; appose.python_worker.main()"); } @@ -179,19 +178,20 @@ default Service java(String mainClass, List classPath, * @throws IOException If something goes wrong starting the worker process. */ default Service service(List exes, String... args) throws IOException { - if (args.length == 0) throw new IllegalArgumentException("No executable given"); - - List dirs = useSystemPath() // - ? Arrays.asList(System.getenv("PATH").split(File.pathSeparator)) // - : Collections.singletonList(base()); + if (exes == null || exes.isEmpty()) throw new IllegalArgumentException("No executable given"); - File exeFile = FilePaths.findExe(dirs, exes); - if (exeFile == null) throw new IllegalArgumentException("No executables found amongst candidates: " + exes); + // Discern path to executable by searching the environment's binPaths. + File exeFile = FilePaths.findExe(binPaths(), exes); + // If exeFile is null, just use the first executable bare, because there + // are scenarios like `pixi run python` where the intended executable will + // only by part of the system path while within the activated environment. + String exe = exeFile == null ? exes.get(0) : exeFile.getCanonicalPath(); - String[] allArgs = new String[args.length + 1]; - System.arraycopy(args, 0, allArgs, 1, args.length); - allArgs[0] = exeFile.getCanonicalPath(); + // Construct final args list: launchArgs + exe + args + List allArgs = new ArrayList<>(launchArgs()); + allArgs.add(exe); + allArgs.addAll(Arrays.asList(args)); - return new Service(new File(base()), allArgs); + return new Service(new File(base()), allArgs.toArray(new String[0])); } } diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index f97e020..eaac23e 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -535,8 +535,8 @@ public void updateIn( final String envName, final String... args ) throws IOExce /** * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * - * @param envName - * The environment name to be created. + * @param envDir + * The directory within which the environment will be created. * @param envYaml * The environment yaml file containing the information required to build it * @throws IOException @@ -547,18 +547,18 @@ public void updateIn( final String envName, final String... args ) throws IOExce * thrown. * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void createWithYaml( final String envName, final String envYaml ) throws IOException, InterruptedException, MambaInstallException + public void createWithYaml( final File envDir, final String envYaml ) throws IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); - createWithYaml(envName, envYaml, false); + createWithYaml(envDir, envYaml, false); } /** * Run {@code conda create} to create a conda environment defined by the input environment yaml file. * - * @param envName - * The environment name to be created. It should not be a path, just the name. + * @param envDir + * The directory within which the environment will be created. * @param envYaml * The environment yaml file containing the information required to build it * @param isForceCreation @@ -574,17 +574,14 @@ public void createWithYaml( final String envName, final String envYaml ) throws * @throws RuntimeException if the process to create the env of the yaml file is not terminated correctly. If there is any error running the commands * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void createWithYaml( final String envName, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException, RuntimeException, MambaInstallException + public void createWithYaml( final File envDir, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException, RuntimeException, MambaInstallException { - if (envName.contains(File.pathSeparator)) - throw new IllegalArgumentException("The environment name should not contain the file separator character: '" - + File.separator + "'"); checkMambaInstalled(); if (!installed) throw new MambaInstallException("Micromamba is not installed"); if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", - getEnvDir(envName), "-f", envYaml, "-y", "-vv" ); + envDir.getAbsolutePath(), "-f", envYaml, "-y", "-vv" ); } /** @@ -780,9 +777,9 @@ private void setEnvName( final String envName ) /** * Returns the active environment name. - * + * * @return The active environment name. - * + * */ public String getEnvName() { @@ -1312,6 +1309,7 @@ public List< String > getEnvironmentNames() throws IOException, MambaInstallExce envs.addAll( Files.list( Paths.get( envsdir ) ) .map( p -> p.getFileName().toString() ) .filter( p -> !p.startsWith( "." ) ) + .filter( p -> Paths.get(p, "conda-meta").toFile().isDirectory() ) .collect( Collectors.toList() ) ); return envs; } diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java new file mode 100644 index 0000000..79f88c1 --- /dev/null +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -0,0 +1,203 @@ +/*- + * #%L + * Appose: multi-language interprocess cooperation with shared memory. + * %% + * Copyright (C) 2023 - 2024 Appose developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.apposed.appose.mamba; + +import org.apposed.appose.BuildHandler; +import org.apposed.appose.FilePaths; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +/** A {@link BuildHandler} plugin powered by micromamba. */ +public class MambaHandler implements BuildHandler { + + private final List channels = new ArrayList<>(); + private final List condaIncludes = new ArrayList<>(); + private final List yamlIncludes = new ArrayList<>(); + private final List pypiIncludes = new ArrayList<>(); + + @Override + public boolean channel(String name, String location) { + if (location == null) { + // Assume it's a conda channel. + channels.add(name); + return true; + } + return false; + } + + @Override + public boolean include(String content, String scheme) { + if (content == null) throw new NullPointerException("content must not be null"); + if (scheme == null) throw new NullPointerException("scheme must not be null"); + switch (scheme) { + case "conda": + // It's a conda package (or newline-separated package list). + condaIncludes.addAll(lines(content)); + return true; + case "pypi": + // It's a PyPI package (or newline-separated package list). + pypiIncludes.addAll(lines(content)); + return true; + case "environment.yml": + yamlIncludes.add(content); + return true; + } + return false; + } + + @Override + public String envName() { + for (String yaml : yamlIncludes) { + String[] lines = yaml.split("(\r\n|\n|\r)"); + Optional name = Arrays.stream(lines) + .filter(line -> line.startsWith("name:")) + .map(line -> line.substring(5).trim().replace("\"", "")) + .findFirst(); + if (name.isPresent()) return name.get(); + } + return null; + } + + @Override + public void build(File envDir, Map> config) throws IOException { + if (!channels.isEmpty() || !condaIncludes.isEmpty() || !pypiIncludes.isEmpty()) { + throw new UnsupportedOperationException( + "Sorry, I don't know how to mix in additional packages from conda or PyPI yet." + + " Please put them in your environment.yml for now."); + } + if (yamlIncludes.isEmpty()) { + // Nothing for this handler to do. + fillConfig(envDir, config); + return; + } + if (yamlIncludes.size() > 1) { + throw new UnsupportedOperationException( + "Sorry, I can't synthesize micromamba environments from multiple environment.yml files yet." + + " Please use a single environment.yml for now."); + } + + // Is this envDir an already-existing conda directory? + // If so, we can update it. + if (new File(envDir, "conda-meta").isDirectory()) { + // This environment has already been populated. + // TODO: Should we update it? For now, we just use it. + fillConfig(envDir, config); + return; + } + + // Micromamba refuses to create an environment into an existing non-conda directory: + // + // "Non-conda folder exists at prefix" + // + // So if a non-conda directory already exists, we need to perform some + // contortions to make micromamba integrate with other build handlers: + // + // 1. If envDir already exists, rename it temporarily. + // 2. Run the micromamba command to create the environment. + // 3. Recursively move any previously existing contents from the + // temporary directory into the newly constructed one. + // 4. If moving an old file would overwrite a new file, put the old + // file back with a .old extension, so nothing is permanently lost. + // 5. As part of the move, remove the temp directories as they empty out. + + // Write out environment.yml from input content. + // We cannot write it into envDir, because mamba needs the directory to + // not exist yet in order to create the environment there. So we write it + // into a temporary work directory with a hopefully unique name. + Path envPath = envDir.getAbsoluteFile().toPath(); + String antiCollision = "" + (new Random().nextInt(90000000) + 10000000); + File workDir = envPath.resolveSibling(envPath.getFileName() + "." + antiCollision + ".tmp").toFile(); + if (envDir.exists()) { + if (envDir.isDirectory()) { + // Move aside the existing non-conda directory. + if (!envDir.renameTo(workDir)) { + throw new IOException("Failed to rename directory: " + envDir + " -> " + workDir); + } + } + else throw new IllegalArgumentException("Non-directory file already exists: " + envDir.getAbsolutePath()); + } + else if (!workDir.mkdirs()) { + throw new IOException("Failed to create work directory: " + workDir); + } + + // At this point, workDir exists and envDir does not. + // We want to write the environment.yml file into the work dir. + // But what if there is an existing environment.yml file in the work dir? + // Let's move it out of the way, rather than stomping on it. + File environmentYaml = new File(workDir, "environment.yml"); + FilePaths.renameToBackup(environmentYaml); + + // It should be safe to write out the environment.yml file now. + try (FileWriter fout = new FileWriter(environmentYaml)) { + fout.write(yamlIncludes.get(0)); + } + + // Finally, we can build the environment from the environment.yml file. + try { + Mamba conda = new Mamba(Mamba.BASE_PATH); + conda.installMicromamba(); + conda.createWithYaml(envDir, environmentYaml.getAbsolutePath()); + } catch (InterruptedException | URISyntaxException | MambaInstallException e) { + throw new IOException(e); + } + + // Lastly, we merge the contents of workDir into envDir. This will be + // at least the environment.yml file, and maybe other files from other handlers. + FilePaths.moveDirectory(workDir, envDir, false); + + fillConfig(envDir, config); + } + + private List lines(String content) { + return Arrays.stream(content.split("(\r\n|\n|\r)")) + .map(String::trim) + .filter(s -> !s.isEmpty() && !s.startsWith("#")) + .collect(Collectors.toList()); + } + + public void fillConfig(File envDir, Map> config) { + // If ${envDir}/bin directory exists, add it to binDirs. + File binDir = new File(envDir, "bin"); + if (binDir.isDirectory()) { + config.computeIfAbsent("binDirs", k -> new ArrayList<>()); + config.get("binDirs").add(binDir.getAbsolutePath()); + } + } +} diff --git a/src/main/resources/META-INF/services/org.apposed.appose.BuildHandler b/src/main/resources/META-INF/services/org.apposed.appose.BuildHandler new file mode 100644 index 0000000..4a96693 --- /dev/null +++ b/src/main/resources/META-INF/services/org.apposed.appose.BuildHandler @@ -0,0 +1 @@ +org.apposed.appose.mamba.MambaHandler diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 263180d..d5cf6e5 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -91,11 +91,13 @@ public void testPython() throws IOException, InterruptedException { public void testConda() throws IOException, InterruptedException { Environment env = Appose.conda(new File("src/test/resources/envs/cowsay.yml")).build(); try (Service service = env.python()) { + //service.debug(System.err::println); Task task = service.task( "import cowsay\n" + "task.outputs['moo'] = cowsay.get_output_string('cow', 'moo')\n" ); task.waitFor(); + assertEquals(TaskStatus.COMPLETE, task.status); String expectedMoo = " ___\n" + "| moo |\n" + @@ -114,7 +116,7 @@ public void testConda() throws IOException, InterruptedException { @Test public void testServiceStartupFailure() throws IOException { - Environment env = Appose.base("no-pythons-to-be-found-here").build(); + Environment env = Appose.build("no-pythons-to-be-found-here"); try (Service service = env.python()) { fail("Python worker process started successfully!?"); } diff --git a/src/test/java/org/apposed/appose/NDArrayExamplePython.java b/src/test/java/org/apposed/appose/NDArrayExamplePython.java index e37582c..bf8fe92 100644 --- a/src/test/java/org/apposed/appose/NDArrayExamplePython.java +++ b/src/test/java/org/apposed/appose/NDArrayExamplePython.java @@ -52,7 +52,7 @@ public static void main(String[] args) throws Exception { } // pass to python (will be wrapped as numpy ndarray - final Environment env = Appose.base( "/opt/homebrew/Caskroom/miniforge/base/envs/appose/" ).build(); + final Environment env = Appose.build("/opt/homebrew/Caskroom/miniforge/base/envs/appose/"); try ( Service service = env.python() ) { final Map< String, Object > inputs = new HashMap<>(); inputs.put( "img", ndArray); From 59c2a47a5e1e3db796d79af85e6b960528592341 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 14:55:17 -0500 Subject: [PATCH 090/120] Remove unused mamba-related functionality --- .../apposed/appose/mamba/CondaException.java | 124 --- .../java/org/apposed/appose/mamba/Mamba.java | 941 +----------------- 2 files changed, 17 insertions(+), 1048 deletions(-) delete mode 100644 src/main/java/org/apposed/appose/mamba/CondaException.java diff --git a/src/main/java/org/apposed/appose/mamba/CondaException.java b/src/main/java/org/apposed/appose/mamba/CondaException.java deleted file mode 100644 index 74fe23f..0000000 --- a/src/main/java/org/apposed/appose/mamba/CondaException.java +++ /dev/null @@ -1,124 +0,0 @@ -/*- - * #%L - * Appose: multi-language interprocess cooperation with shared memory. - * %% - * Copyright (C) 2023 - 2024 Appose developers. - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.apposed.appose.mamba; - -public class CondaException -{ - - public static class EnvironmentExistsException extends RuntimeException - { - private static final long serialVersionUID = -1625119813967214783L; - - /** - * Constructs a new exception with {@code null} as its detail message. The cause - * is not initialized, and may subsequently be initialized by a call to - * {@link #initCause}. - */ - public EnvironmentExistsException() - { - super(); - } - - /** - * Constructs a new exception with the specified detail message. The cause is - * not initialized, and may subsequently be initialized by a call to - * {@link #initCause}. - * - * @param msg - * the detail message. The detail message is saved for later - * retrieval by the {@link #getMessage()} method. - */ - public EnvironmentExistsException( String msg ) - { - super( msg ); - } - - /** - * Constructs a new exception with the specified detail message and cause. - *

- * Note that the detail message associated with {@code cause} is not - * automatically incorporated in this exception's detail message. - * - * @param message - * the detail message (which is saved for later retrieval by the - * {@link #getMessage()} method). - * @param cause - * the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is permitted, - * and indicates that the cause is nonexistent or unknown.) - * @since 1.4 - */ - public EnvironmentExistsException( String message, Throwable cause ) - { - super( message, cause ); - } - - /** - * Constructs a new exception with the specified cause and a detail message of - * (cause==null ? null : cause.toString()) (which typically contains - * the class and detail message of cause). This constructor is useful - * for exceptions that are little more than wrappers for other throwables (for - * example, {@link java.security.PrivilegedActionException}). - * - * @param cause - * the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is permitted, - * and indicates that the cause is nonexistent or unknown.) - * @since 1.4 - */ - public EnvironmentExistsException( Throwable cause ) - { - super( cause ); - } - - /** - * Constructs a new exception with the specified detail message, cause, - * suppression enabled or disabled, and writable stack trace enabled or - * disabled. - * - * @param message - * the detail message. - * @param cause - * the cause. (A {@code null} value is permitted, and indicates that - * the cause is nonexistent or unknown.) - * @param enableSuppression - * whether or not suppression is enabled or disabled - * @param writableStackTrace - * whether or not the stack trace should be writable - * @since 1.7 - */ - protected EnvironmentExistsException( String message, Throwable cause, - boolean enableSuppression, - boolean writableStackTrace ) - { - super( message, cause, enableSuppression, writableStackTrace ); - } - } - -} diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index eaac23e..b5004ca 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -59,13 +59,8 @@ package org.apposed.appose.mamba; -import org.apache.commons.compress.utils.FileNameUtils; import org.apache.commons.lang3.SystemUtils; -import com.sun.jna.Platform; - -import org.apposed.appose.mamba.CondaException.EnvironmentExistsException; - import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -82,9 +77,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; @@ -102,10 +95,6 @@ public class Mamba { * String containing the path that points to the micromamba executable */ final String mambaCommand; - /** - * Name of the environment where the changes are going to be applied - */ - private String envName; /** * Root directory of micromamba that also contains the environments folder * @@ -124,10 +113,6 @@ public class Mamba { * Path to the folder that contains the directories */ private final String envsdir; - /** - * Whether Micromamba is installed or not - */ - private boolean installed = false; /** * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. * @@ -173,14 +158,6 @@ public class Mamba { * This consumer saves all the log of every micromamba execution */ private final Consumer errConsumer = this::updateErrorConsumer; - /* - * Path to Python executable from the environment directory - */ - final static String PYTHON_COMMAND = SystemUtils.IS_OS_WINDOWS ? "python.exe" : "bin/python"; - /** - * Default name for a Python environment - */ - public final static String DEFAULT_ENVIRONMENT_NAME = "base"; /** * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ @@ -323,24 +300,29 @@ public Mamba(final String rootdir) { } catch (Exception ex) { return; } - installed = true; } - + /** - * Check whether micromamba is installed or not to be able to use the instance of {@link Mamba} + * Gets whether micromamba is installed or not to be able to use the instance of {@link Mamba} * @return whether micromamba is installed or not to be able to use the instance of {@link Mamba} */ - public boolean checkMambaInstalled() { + public boolean isMambaInstalled() { try { getVersion(); - this.installed = true; - } catch (Exception ex) { - this.installed = false; + return true; + } catch (IOException | InterruptedException e) { return false; - } - return true; + } + } + + /** + * Check whether micromamba is installed or not to be able to use the instance of {@link Mamba} + * @throws MambaInstallException if micromamba is not installed + */ + private void checkMambaInstalled() throws MambaInstallException { + if (!isMambaInstalled()) throw new MambaInstallException("Micromamba is not installed"); } - + /** * * @return the progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. @@ -445,21 +427,10 @@ private void decompressMicromamba(final File tempFile) * @throws URISyntaxException if there is any error with the micromamba url */ public void installMicromamba() throws IOException, InterruptedException, URISyntaxException { - checkMambaInstalled(); - if (installed) return; + if (isMambaInstalled()) return; decompressMicromamba(downloadMicromamba()); - checkMambaInstalled(); } - public static String envNameFromYaml(File condaEnvironmentYaml) throws IOException { - List lines = Files.readAllLines(condaEnvironmentYaml.toPath()); - return lines.stream() - .filter(line -> line.startsWith("name:")) - .map(line -> line.substring(5).trim()) - .findFirst() - .orElseGet(() -> FileNameUtils.getBaseName(condaEnvironmentYaml.toPath())); - } - public String getEnvsDir() { return this.envsdir; } @@ -483,28 +454,6 @@ private static List< String > getBaseCommand() return cmd; } - /** - * Run {@code conda update} in the activated environment. A list of packages to - * be updated and extra parameters can be specified as {@code args}. - * - * @param args - * The list of packages to be updated and extra parameters as - * {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void update( final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - updateIn( envName, args ); - } - /** * Run {@code conda update} in the specified environment. A list of packages to * update and extra parameters can be specified as {@code args}. @@ -525,7 +474,6 @@ public void update( final String... args ) throws IOException, InterruptedExcept public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", getEnvDir(envName) ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); @@ -550,36 +498,6 @@ public void updateIn( final String envName, final String... args ) throws IOExce public void createWithYaml( final File envDir, final String envYaml ) throws IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - createWithYaml(envDir, envYaml, false); - } - - /** - * Run {@code conda create} to create a conda environment defined by the input environment yaml file. - * - * @param envDir - * The directory within which the environment will be created. - * @param envYaml - * The environment yaml file containing the information required to build it - * @param isForceCreation - * Force creation of the environment if {@code true}. If this value - * is {@code false} and an environment with the specified name - * already exists, throw an {@link EnvironmentExistsException}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws RuntimeException if the process to create the env of the yaml file is not terminated correctly. If there is any error running the commands - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void createWithYaml( final File envDir, final String envYaml, final boolean isForceCreation) throws IOException, InterruptedException, RuntimeException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) - throw new EnvironmentExistsException(); runMamba("env", "create", "--prefix", envDir.getAbsolutePath(), "-f", envYaml, "-y", "-vv" ); } @@ -600,35 +518,6 @@ public void createWithYaml( final File envDir, final String envYaml, final boole public void create( final String envName ) throws IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - create( envName, false ); - } - - /** - * Run {@code conda create} to create an empty conda environment. - * - * @param envName - * The environment name to be created. - * @param isForceCreation - * Force creation of the environment if {@code true}. If this value - * is {@code false} and an environment with the specified name - * already exists, throw an {@link EnvironmentExistsException}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws RuntimeException - * If there is any error running the commands - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void create( final String envName, final boolean isForceCreation ) throws IOException, InterruptedException, RuntimeException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) - throw new EnvironmentExistsException(); runMamba( "create", "-y", "-p", getEnvDir(envName) ); } @@ -652,37 +541,6 @@ public void create( final String envName, final boolean isForceCreation ) throws public void create( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - create( envName, false, args ); - } - - /** - * Run {@code conda create} to create a new conda environment with a list of - * specified packages. - * - * @param envName - * The environment name to be created. - * @param isForceCreation - * Force creation of the environment if {@code true}. If this value - * is {@code false} and an environment with the specified name - * already exists, throw an {@link EnvironmentExistsException}. - * @param args - * The list of packages to be installed on environment creation and - * extra parameters as {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void create( final String envName, final boolean isForceCreation, final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) - throw new EnvironmentExistsException(); final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); @@ -695,10 +553,6 @@ public void create( final String envName, final boolean isForceCreation, final S * * @param envName * The environment name to be created. CAnnot be null. - * @param isForceCreation - * Force creation of the environment if {@code true}. If this value - * is {@code false} and an environment with the specified name - * already exists, throw an {@link EnvironmentExistsException}. * @param channels * the channels from where the packages can be installed. Can be null * @param packages @@ -714,13 +568,10 @@ public void create( final String envName, final boolean isForceCreation, final S * If there is any error running the commands * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException + public void create( final String envName, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); - if ( !isForceCreation && getEnvironmentNames().contains( envName ) ) - throw new EnvironmentExistsException(); final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); if (channels == null) channels = new ArrayList<>(); for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} @@ -730,354 +581,6 @@ public void create( final String envName, final boolean isForceCreation, List channels, List packages ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - installIn( envName, channels, packages ); - } - - /** - * Run {@code conda install} in the specified environment. A list of packages to - * install and extra parameters can be specified as {@code args}. - * - * @param envName - * The environment name to be used for the install command. - * @param channels - * the channels from where the packages can be installed. Can be null - * @param packages - * the packages that want to be installed during env creation. They can contain the version. - * For example, "python" or "python=3.10.1", "numpy" or "numpy=1.20.1". CAn be null if no packages want to be installed - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws RuntimeException if the process to create the env of the yaml file is not terminated correctly. If there is any error running the commands - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void installIn( final String envName, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-y", "-p", getEnvDir(envName) ) ); - if (channels == null) channels = new ArrayList<>(); - for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} - if (packages == null) packages = new ArrayList<>(); - cmd.addAll(packages); - runMamba(cmd.toArray(new String[0])); - } - - /** - * Run {@code conda install} in the specified environment. A list of packages to - * install and extra parameters can be specified as {@code args}. - * - * @param envName - * The environment name to be used for the install command. - * @param args - * The list of packages to be installed and extra parameters as - * {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void installIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > cmd = new ArrayList<>( Arrays.asList( "install", "-p", getEnvDir(envName) ) ); - cmd.addAll( Arrays.asList( args ) ); - if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); - runMamba(cmd.toArray(new String[0])); - } - - /** - * Run {@code pip install} in the activated environment. A list of packages to - * install and extra parameters can be specified as {@code args}. - * - * @param args - * The list of packages to be installed and extra parameters as - * {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void pipInstall( final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - pipInstallIn( envName, args ); - } - - /** - * Run {@code pip install} in the specified environment. A list of packages to - * install and extra parameters can be specified as {@code args}. - * - * @param envName - * The environment name to be used for the install command. - * @param args - * The list of packages to be installed and extra parameters as - * {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void pipInstallIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > cmd = new ArrayList<>( Arrays.asList( "-m", "pip", "install" ) ); - cmd.addAll( Arrays.asList( args ) ); - runPythonIn( envName, cmd.toArray(new String[0])); - } - - /** - * Run a Python command in the activated environment. This method automatically - * sets environment variables associated with the activated environment. In - * Windows, this method also sets the {@code PATH} environment variable so that - * the specified environment runs as expected. - * - * @param args - * One or more arguments for the Python command. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void runPython( final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - runPythonIn( envName, args ); - } - - /** - * Runs a Python command in the specified environment. This method automatically - * sets environment variables associated with the specified environment. In - * Windows, this method also sets the {@code PATH} environment variable so that - * the specified environment runs as expected. - *

- * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - * TODO stop process if the thread is interrupted, same as with mamba, look for runmamna method for example - *

- * - * @param envName - * The environment name used to run the Python command. - * @param args - * One or more arguments for the Python command. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void runPythonIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > cmd = getBaseCommand(); - List argsList = new ArrayList<>(); - String envDir; - if (new File(envName, PYTHON_COMMAND).isFile()) { - argsList.add( coverArgWithDoubleQuotes(Paths.get( envName, PYTHON_COMMAND ).toAbsolutePath().toString()) ); - envDir = Paths.get( envName ).toAbsolutePath().toString(); - } else if (Paths.get( getEnvDir(envName), PYTHON_COMMAND ).toFile().isFile()) { - argsList.add( coverArgWithDoubleQuotes(Paths.get( getEnvDir(envName), PYTHON_COMMAND ).toAbsolutePath().toString()) ); - envDir = Paths.get( getEnvDir(envName) ).toAbsolutePath().toString(); - } else - throw new IOException("The environment provided (" - + envName + ") does not exist or does not contain a Python executable (" + PYTHON_COMMAND + ")."); - argsList.addAll( Arrays.stream( args ).map(aa -> { - if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); - else return aa; - }).collect(Collectors.toList()) ); - boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); - - if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); - else cmd.add(surroundWithQuotes(argsList)); - - final ProcessBuilder builder = getBuilder( true ); - if ( SystemUtils.IS_OS_WINDOWS ) - { - final Map< String, String > envs = builder.environment(); - envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Scripts" ) + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library" ) + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library", "Bin" ) + ";" + envs.get( "Path" ) ); - } - // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); - if ( builder.command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException("Error executing the following command: " + builder.command()); - } - - /** - * Run a Python command in the specified environment. This method automatically - * sets environment variables associated with the specified environment. In - * Windows, this method also sets the {@code PATH} environment variable so that - * the specified environment runs as expected. - * - * @param envFile - * file corresponding to the environment directory - * @param args - * One or more arguments for the Python command. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - public static void runPythonIn( final File envFile, final String... args ) throws IOException, InterruptedException - { - if (!Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toFile().isFile()) - throw new IOException("No Python found in the environment provided. The following " - + "file does not exist: " + Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath()); - final List< String > cmd = getBaseCommand(); - List argsList = new ArrayList<>(); - argsList.add( coverArgWithDoubleQuotes(Paths.get( envFile.getAbsolutePath(), PYTHON_COMMAND ).toAbsolutePath().toString()) ); - argsList.addAll( Arrays.stream( args ).map(aa -> { - if (Platform.isWindows() && aa.contains(" ")) return coverArgWithDoubleQuotes(aa); - else return aa; - }).collect(Collectors.toList()) ); - boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); - - if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); - else cmd.add(surroundWithQuotes(argsList)); - - - final ProcessBuilder builder = new ProcessBuilder().directory( envFile ); - builder.inheritIO(); - if ( SystemUtils.IS_OS_WINDOWS ) - { - final Map< String, String > envs = builder.environment(); - final String envDir = envFile.getAbsolutePath(); - envs.put( "Path", envDir + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Scripts" ) + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library" ) + ";" + envs.get( "Path" ) ); - envs.put( "Path", Paths.get( envDir, "Library", "Bin" ) + ";" + envs.get( "Path" ) ); - } - // TODO find way to get env vars in micromamba builder.environment().putAll( getEnvironmentVariables( envName ) ); - if ( builder.command( cmd ).start().waitFor() != 0 ) - throw new RuntimeException("Error executing the following command: " + builder.command()); - } - /** * Returns Conda version as a {@code String}. * @@ -1122,7 +625,6 @@ public String getVersion() throws IOException, InterruptedException { public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); Thread mainThread = Thread.currentThread(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); @@ -1233,410 +735,9 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException { checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); runMamba(false, args); } - /** - * Returns environment variables associated with the activated environment as - * {@code Map< String, String >}. - * - * @return The environment variables as {@code Map< String, String >}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - /* TODO find equivalent in mamba - public Map< String, String > getEnvironmentVariables() throws IOException, InterruptedException - { - return getEnvironmentVariables( envName ); - } - */ - - /** - * Returns environment variables associated with the specified environment as - * {@code Map< String, String >}. - * - * @param envName - * The environment name used to run the Python command. - * @return The environment variables as {@code Map< String, String >}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - */ - /** - * TODO find equivalent in mamba - public Map< String, String > getEnvironmentVariables( final String envName ) throws IOException, InterruptedException - { - final List< String > cmd = getBaseCommand(); - cmd.addAll( Arrays.asList( condaCommand, "env", "config", "vars", "list", "-n", envName ) ); - final Process process = getBuilder( false ).command( cmd ).start(); - if ( process.waitFor() != 0 ) - throw new RuntimeException(); - final Map< String, String > map = new HashMap<>(); - try (final BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) )) - { - String line; - - while ( ( line = reader.readLine() ) != null ) - { - final String[] keyVal = line.split( " = " ); - map.put( keyVal[ 0 ], keyVal[ 1 ] ); - } - } - return map; - } - */ - - /** - * Returns a list of the Mamba environment names as {@code List< String >}. - * - * @return The list of the Mamba environment names as {@code List< String >}. - * @throws IOException If an I/O error occurs. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public List< String > getEnvironmentNames() throws IOException, MambaInstallException - { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - final List< String > envs = new ArrayList<>(Collections.singletonList(DEFAULT_ENVIRONMENT_NAME)); - envs.addAll( Files.list( Paths.get( envsdir ) ) - .map( p -> p.getFileName().toString() ) - .filter( p -> !p.startsWith( "." ) ) - .filter( p -> Paths.get(p, "conda-meta").toFile().isDirectory() ) - .collect( Collectors.toList() ) ); - return envs; - } - - /** - * Check whether a list of dependencies provided is installed in the wanted environment. - * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependencies - * The list of dependencies that should be installed in the environment. - * They can contain version requirements. The names should be the ones used to import the package inside python, - * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" - * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" - * @return true if the packages are installed or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkAllDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - return checkUninstalledDependenciesInEnv(envName, dependencies).isEmpty(); - } - - /** - * Returns a list containing the packages that are not installed in the wanted environment - * from the list of dependencies provided - * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependencies - * The list of dependencies that should be installed in the environment. - * They can contain version requirements. The names should be the ones used to import the package inside python, - * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" - * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" - * @return the list of packages that are not already installed - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public List checkUninstalledDependenciesInEnv(String envName, List dependencies) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - File envFile = new File(this.envsdir, envName); - File envFile2 = new File(envName); - if (!envFile.isDirectory() && !envFile2.isDirectory()) - return dependencies; - return dependencies.stream().filter(dep -> { - try { - return !checkDependencyInEnv(envName, dep); - } catch (Exception ex) { - return true; - } - }).collect(Collectors.toList()); - } - - /** - * Checks whether a package is installed in the wanted environment. - * - * @param envName - * The name of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependency - * The name of the package that should be installed in the env - * They can contain version requirements. The names should be the ones used to import the package inside python, - * "skimage", not "scikit-image" or "sklearn", not "scikit-learn" - * An example list: "numpy", "numba>=0.43.1", "torch==1.6", "torch>=1.6, <2.0" - * @return true if the package is installed or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkDependencyInEnv(String envName, String dependency) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if (dependency.contains("==")) { - int ind = dependency.indexOf("=="); - return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim()); - } else if (dependency.contains(">=") && dependency.contains("<=") && dependency.contains(",")) { - int commaInd = dependency.indexOf(","); - int highInd = dependency.indexOf(">="); - int lowInd = dependency.indexOf("<="); - int minInd = Math.min(Math.min(commaInd, lowInd), highInd); - String packName = dependency.substring(0, minInd).trim(); - String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); - String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envName, packName, minV, maxV, false); - } else if (dependency.contains(">=") && dependency.contains("<") && dependency.contains(",")) { - int commaInd = dependency.indexOf(","); - int highInd = dependency.indexOf(">="); - int lowInd = dependency.indexOf("<"); - int minInd = Math.min(Math.min(commaInd, lowInd), highInd); - String packName = dependency.substring(0, minInd).trim(); - String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); - String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envName, packName, minV, null, false) && checkDependencyInEnv(envName, packName, null, maxV, true); - } else if (dependency.contains(">") && dependency.contains("<=") && dependency.contains(",")) { - int commaInd = dependency.indexOf(","); - int highInd = dependency.indexOf(">"); - int lowInd = dependency.indexOf("<="); - int minInd = Math.min(Math.min(commaInd, lowInd), highInd); - String packName = dependency.substring(0, minInd).trim(); - String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); - String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envName, packName, minV, null, true) && checkDependencyInEnv(envName, packName, null, maxV, false); - } else if (dependency.contains(">") && dependency.contains("<") && dependency.contains(",")) { - int commaInd = dependency.indexOf(","); - int highInd = dependency.indexOf(">"); - int lowInd = dependency.indexOf(">"); - int minInd = Math.min(Math.min(commaInd, lowInd), highInd); - String packName = dependency.substring(0, minInd).trim(); - String minV = dependency.substring(lowInd + 1, lowInd < highInd ? commaInd : dependency.length()); - String maxV = dependency.substring(highInd + 1, lowInd < highInd ? dependency.length() : commaInd); - return checkDependencyInEnv(envName, packName, minV, maxV, true); - } else if (dependency.contains(">")) { - int ind = dependency.indexOf(">"); - return checkDependencyInEnv(envName, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), true); - } else if (dependency.contains(">=")) { - int ind = dependency.indexOf(">="); - return checkDependencyInEnv(envName, null, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), false); - } else if (dependency.contains("<=")) { - int ind = dependency.indexOf("<="); - return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 2).trim(), null, false); - } else if (dependency.contains("<")) { - int ind = dependency.indexOf("<"); - return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim(), null, true); - } else if (dependency.contains("=")) { - int ind = dependency.indexOf("="); - return checkDependencyInEnv(envName, dependency.substring(0, ind).trim(), dependency.substring(ind + 1).trim()); - }else { - return checkDependencyInEnv(envName, dependency, null); - } - } - - /** - * Checks whether a package of a specific version is installed in the wanted environment. - * - * @param envDir - * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependency - * The name of the package that should be installed in the env. The String should only contain the name, no version, - * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" - * or "sklearn", not "scikit-learn". - * @param version - * the specific version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0" - * @return true if the package is installed or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkDependencyInEnv(String envDir, String dependency, String version) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - return checkDependencyInEnv(envDir, dependency, version, version, true); - } - - /** - * Checks whether a package with specific version constraints is installed in the wanted environment. - * In this method the minversion argument should be strictly smaller than the version of interest and - * the maxversion strictly bigger. - * This method checks that: dependency >minversion, <maxversion - * For smaller or equal or bigger or equal (dependency >=minversion, <=maxversion) look at the method - * {@link #checkDependencyInEnv(String, String, String, String, boolean)} with the lst parameter set to false. - * - * @param envDir - * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependency - * The name of the package that should be installed in the env. The String should only contain the name, no version, - * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" - * or "sklearn", not "scikit-learn". - * @param minversion - * the minimum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". - * This version should be strictly smaller than the one of interest, if for example "1.9" is given, it is assumed that - * package_version>1.9. - * If there is no minimum version requirement for the package of interest, set this argument to null. - * @param maxversion - * the maximum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". - * This version should be strictly bigger than the one of interest, if for example "1.9" is given, it is assumed that - * package_version<1.9. - * If there is no maximum version requirement for the package of interest, set this argument to null. - * @return true if the package is installed or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, String maxversion) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - return checkDependencyInEnv(envDir, dependency, minversion, maxversion, true); - } - - /** - * Checks whether a package with specific version constraints is installed in the wanted environment. - * Depending on the last argument ('strictlyBiggerOrSmaller') 'minversion' and 'maxversion' - * will be strictly bigger(>=) or smaller(<) or bigger or equal >=) or smaller or equal<=) - * In this method the minversion argument should be strictly smaller than the version of interest and - * the maxversion strictly bigger. - * - * @param envDir - * The directory of the environment of interest. Should be one of the environments of the current Mamba instance. - * This parameter can also be the full path to an independent environment. - * @param dependency - * The name of the package that should be installed in the env. The String should only contain the name, no version, - * and the name should be the one used to import the package inside python. For example, "skimage", not "scikit-image" - * or "sklearn", not "scikit-learn". - * @param minversion - * the minimum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". - * If there is no minimum version requirement for the package of interest, set this argument to null. - * @param maxversion - * the maximum required version of the package that needs to be installed. For example:, "0.43.1", "1.6", "2.0". - * If there is no maximum version requirement for the package of interest, set this argument to null. - * @param strictlyBiggerOrSmaller - * Whether the minversion and maxversion shuld be strictly smaller and bigger or not - * @return true if the package is installed or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkDependencyInEnv(String envDir, String dependency, String minversion, - String maxversion, boolean strictlyBiggerOrSmaller) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - File envFile = new File(this.envsdir, envDir); - File envFile2 = new File(envDir); - if (!envFile.isDirectory() && !envFile2.isDirectory()) - return false; - else if (!envFile.isDirectory()) - envFile = envFile2; - if (dependency.trim().equals("python")) return checkPythonInstallation(envDir, minversion, maxversion, strictlyBiggerOrSmaller); - String checkDepCode; - if (minversion != null && maxversion != null && minversion.equals(maxversion)) { - checkDepCode = "import importlib.util, sys; " - + "from importlib.metadata import version; " - + "from packaging import version as vv; " - + "pkg = '%s'; wanted_v = '%s'; " - + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) == vv.parse(wanted_v) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, maxversion); - } else if (minversion == null && maxversion == null) { - checkDepCode = "import importlib.util, sys; sys.exit(0) if importlib.util.find_spec('%s') else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency); - } else if (maxversion == null) { - checkDepCode = "import importlib.util, sys; " - + "from importlib.metadata import version; " - + "from packaging import version as vv; " - + "pkg = '%s'; desired_version = '%s'; " - + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, minversion, strictlyBiggerOrSmaller ? ">" : ">="); - } else if (minversion == null) { - checkDepCode = "import importlib.util, sys; " - + "from importlib.metadata import version; " - + "from packaging import version as vv; " - + "pkg = '%s'; desired_version = '%s'; " - + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(desired_version) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); - } else { - checkDepCode = "import importlib.util, sys; " - + "from importlib.metadata import version; " - + "from packaging import version as vv; " - + "pkg = '%s'; min_v = '%s'; max_v = '%s'; " - + "spec = importlib.util.find_spec(pkg); " - + "sys.exit(0) if spec and vv.parse(version(pkg)) %s vv.parse(min_v) and vv.parse(version(pkg)) %s vv.parse(max_v) else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, dependency, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); - } - try { - runPythonIn(envFile, "-c", checkDepCode); - } catch (RuntimeException | IOException | InterruptedException e) { - return false; - } - return true; - } - - private boolean checkPythonInstallation(String envDir, String minversion, String maxversion, boolean strictlyBiggerOrSmaller) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - File envFile = new File(this.envsdir, envDir); - File envFile2 = new File(envDir); - if (!envFile.isDirectory() && !envFile2.isDirectory()) - return false; - else if (!envFile.isDirectory()) - envFile = envFile2; - String checkDepCode; - if (minversion != null && maxversion != null && minversion.equals(maxversion)) { - checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major" - + " and vv.parse(platform.python_version()).minor == vv.parse(desired_version).minor else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, maxversion); - } else if (minversion == null && maxversion == null) { - checkDepCode = "2 + 2"; - } else if (maxversion == null) { - checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " - + "and vv.parse(platform.python_version()).minor %s vv.parse(desired_version).minor else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, minversion, strictlyBiggerOrSmaller ? ">" : ">="); - } else if (minversion == null) { - checkDepCode = "import sys; import platform; from packaging import version as vv; desired_version = '%s'; " - + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " - + "and vv.parse(platform.python_version()).minor %s vv.parse(desired_version).minor else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, maxversion, strictlyBiggerOrSmaller ? "<" : "<="); - } else { - checkDepCode = "import platform; " - + "from packaging import version as vv; min_v = '%s'; max_v = '%s'; " - + "sys.exit(0) if vv.parse(platform.python_version()).major == vv.parse(desired_version).major " - + "and vv.parse(platform.python_version()).minor %s vv.parse(min_v).minor " - + "and vv.parse(platform.python_version()).minor %s vv.parse(max_v).minor else sys.exit(1)"; - checkDepCode = String.format(checkDepCode, minversion, maxversion, strictlyBiggerOrSmaller ? ">" : ">=", strictlyBiggerOrSmaller ? "<" : ">="); - } - try { - runPythonIn(envFile, "-c", checkDepCode); - } catch (RuntimeException | IOException | InterruptedException e) { - return false; - } - return true; - } - - /** - * TODO figure out whether to use a dependency or not to parse the yaml file - * @param envYaml - * the path to the yaml file where a Python environment should be specified - * @return true if the env exists or false otherwise - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public boolean checkEnvFromYamlExists(String envYaml) throws MambaInstallException { - checkMambaInstalled(); - if (!installed) throw new MambaInstallException("Micromamba is not installed"); - if (envYaml == null || !new File(envYaml).isFile() - || (envYaml.endsWith(".yaml") && envYaml.endsWith(".yml"))) { - return false; - } - return false; - } - /** * In Windows, if a command prompt argument contains and space " " it needs to * start and end with double quotes @@ -1672,12 +773,4 @@ private static String surroundWithQuotes(List args) { arg += "\""; return arg; } - - public static void main(String[] args) throws IOException, InterruptedException, MambaInstallException { - - Mamba m = new Mamba("C:\\Users\\angel\\Desktop\\Fiji app\\appose_x86_64"); - String envName = "efficientvit_sam_env"; - m.pipInstallIn(envName, - m.getEnvDir(envName) + File.separator + "appose-python"); - } } From ab26a3a8050817f5c6fc20b0645163e406ec66b2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:05:10 -0500 Subject: [PATCH 091/120] Make internal classes package-private So that we don't leak them as public API usable externally. --- src/main/java/org/apposed/appose/mamba/FileDownloader.java | 2 +- src/main/java/org/apposed/appose/mamba/Mamba.java | 2 +- src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/FileDownloader.java b/src/main/java/org/apposed/appose/mamba/FileDownloader.java index b636726..315e6b4 100644 --- a/src/main/java/org/apposed/appose/mamba/FileDownloader.java +++ b/src/main/java/org/apposed/appose/mamba/FileDownloader.java @@ -32,7 +32,7 @@ import java.io.IOException; import java.nio.channels.ReadableByteChannel; -public class FileDownloader { +class FileDownloader { private static final long CHUNK_SIZE = 1024 * 1024 * 5; private final ReadableByteChannel rbc; diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index b5004ca..f57f18d 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -89,7 +89,7 @@ * @author Ko Sugawara * @author Carlos Garcia */ -public class Mamba { +class Mamba { /** * String containing the path that points to the micromamba executable diff --git a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java index 64353bb..3fe3ce8 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java +++ b/src/main/java/org/apposed/appose/mamba/MambaInstallerUtils.java @@ -54,7 +54,7 @@ /** * Utility methods unzip bzip2 files and to enable the download of micromamba */ -public final class MambaInstallerUtils { +final class MambaInstallerUtils { private MambaInstallerUtils() { // Prevent instantiation of utility class. From 40e81f79cc797097f3a955263fa41b4303f900e9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:07:10 -0500 Subject: [PATCH 092/120] Generalize mamba doc from "Python" to "Conda" Conda has packages beyond only Python ones. --- src/main/java/org/apposed/appose/mamba/Mamba.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index f57f18d..58991f6 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -84,7 +84,7 @@ import java.util.stream.Collectors; /** - * Python environment manager, implemented by delegating to micromamba. + * Conda-based environment manager, implemented by delegating to micromamba. * * @author Ko Sugawara * @author Carlos Garcia @@ -125,7 +125,7 @@ class Mamba { private Double mambaDecompressProgress = 0.0; /** * Consumer that tracks the progress in the download of micromamba, the software used - * by this class to manage Python environments + * by this class to manage Conda environments */ private final Consumer mambaDnwldProgressConsumer = this::updateMambaDnwldProgress; /** @@ -169,7 +169,7 @@ class Mamba { */ final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); /** - * Name of the folder inside the {@link #rootdir} that contains the different Python environments created by the Appose Micromamba + * Name of the folder inside the {@link #rootdir} that contains the different Conda environments created by the Appose Micromamba */ final public static String ENVS_NAME = "envs"; /** @@ -481,7 +481,7 @@ public void updateIn( final String envName, final String... args ) throws IOExce } /** - * Run {@code conda create} to create a conda environment defined by the input environment yaml file. + * Run {@code conda create} to create a Conda environment defined by the input environment yaml file. * * @param envDir * The directory within which the environment will be created. From 642ac7fdba55cceb1b185d4981be835ede5464f0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:09:12 -0500 Subject: [PATCH 093/120] Eschew use of platform-sensitive File.separator We can use the Paths utility class instead. --- src/main/java/org/apposed/appose/mamba/Mamba.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 58991f6..e4414ec 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -162,8 +162,8 @@ class Mamba { * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? - File.separator + "Library" + File.separator + "bin" + File.separator + "micromamba.exe" - : File.separator + "bin" + File.separator + "micromamba"; + Paths.get("Library", "bin", "micromamba.exe").toString() : + Paths.get("bin", "micromamba").toString(); /** * Path where Appose installs Micromamba by default */ @@ -436,7 +436,7 @@ public String getEnvsDir() { } public String getEnvDir(String envName) { - return getEnvsDir() + File.separator + envName; + return Paths.get(getEnvsDir(), envName).toString(); } /** From fa91a8c8f55222b888eff3faaa406c941db63e1e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:12:14 -0500 Subject: [PATCH 094/120] Slim down the Mamba code some more --- .../java/org/apposed/appose/mamba/Mamba.java | 109 +++--------------- .../apposed/appose/mamba/MambaHandler.java | 2 +- .../appose/mamba/MambaInstallException.java | 56 --------- 3 files changed, 15 insertions(+), 152 deletions(-) delete mode 100644 src/main/java/org/apposed/appose/mamba/MambaInstallException.java diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index e4414ec..f78fddd 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -78,7 +78,6 @@ import java.util.Arrays; import java.util.Calendar; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -246,7 +245,7 @@ private ProcessBuilder getBuilder( final boolean isInheritIO ) /** * Create a new {@link Mamba} object. The root dir for the Micromamba installation * will be the default base path defined at {@link #BASE_PATH} - * If there is no Micromamba found at the base path {@link #BASE_PATH}, a {@link MambaInstallException} will be thrown + * If there is no Micromamba found at the base path {@link #BASE_PATH}, an {@link IllegalStateException} will be thrown *

* It is expected that the Micromamba installation has executable commands as shown below: *

@@ -268,7 +267,7 @@ public Mamba() { * Create a new Conda object. The root dir for Conda installation can be * specified as {@code String}. * If there is no Micromamba found at the specified path, it will be installed automatically - * if the parameter 'installIfNeeded' is true. If not a {@link MambaInstallException} will be thrown. + * if the parameter 'installIfNeeded' is true. If not an {@link IllegalStateException} will be thrown. *

* It is expected that the Conda installation has executable commands as shown below: *

@@ -317,10 +316,10 @@ public boolean isMambaInstalled() { /** * Check whether micromamba is installed or not to be able to use the instance of {@link Mamba} - * @throws MambaInstallException if micromamba is not installed + * @throws IllegalStateException if micromamba is not installed */ - private void checkMambaInstalled() throws MambaInstallException { - if (!isMambaInstalled()) throw new MambaInstallException("Micromamba is not installed"); + private void checkMambaInstalled() { + if (!isMambaInstalled()) throw new IllegalStateException("Micromamba is not installed"); } /** @@ -398,8 +397,7 @@ private File downloadMicromamba() throws IOException, URISyntaxException { return tempFile; } - private void decompressMicromamba(final File tempFile) - throws IOException, InterruptedException { + private void decompressMicromamba(final File tempFile) throws IOException, InterruptedException { final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); @@ -469,9 +467,9 @@ private static List< String > getBaseCommand() * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used + * @throws IllegalStateException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException + public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException { checkMambaInstalled(); final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", getEnvDir(envName) ) ); @@ -493,94 +491,15 @@ public void updateIn( final String envName, final String... args ) throws IOExce * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used + * @throws IllegalStateException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void createWithYaml( final File envDir, final String envYaml ) throws IOException, InterruptedException, MambaInstallException + public void createWithYaml( final File envDir, final String envYaml ) throws IOException, InterruptedException { checkMambaInstalled(); runMamba("env", "create", "--prefix", envDir.getAbsolutePath(), "-f", envYaml, "-y", "-vv" ); } - /** - * Run {@code conda create} to create an empty conda environment. - * - * @param envName - * The environment name to be created. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void create( final String envName ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - runMamba( "create", "-y", "-p", getEnvDir(envName) ); - } - - /** - * Run {@code conda create} to create a new mamba environment with a list of - * specified packages. - * - * @param envName - * The environment name to be created. - * @param args - * The list of packages to be installed on environment creation and - * extra parameters as {@code String...}. - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void create( final String envName, final String... args ) throws IOException, InterruptedException, MambaInstallException - { - checkMambaInstalled(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); - cmd.addAll( Arrays.asList( args ) ); - if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); - runMamba(cmd.toArray(new String[0])); - } - - /** - * Run {@code conda create} to create a new conda environment with a list of - * specified packages. - * - * @param envName - * The environment name to be created. CAnnot be null. - * @param channels - * the channels from where the packages can be installed. Can be null - * @param packages - * the packages that want to be installed during env creation. They can contain the version. - * For example, "python" or "python=3.10.1", "numpy" or "numpy=1.20.1". CAn be null if no packages want to be installed - * @throws IOException - * If an I/O error occurs. - * @throws InterruptedException - * If the current thread is interrupted by another thread while it - * is waiting, then the wait is ended and an InterruptedException is - * thrown. - * @throws RuntimeException - * If there is any error running the commands - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used - */ - public void create( final String envName, List channels, List packages ) throws IOException, InterruptedException, RuntimeException, MambaInstallException - { - checkMambaInstalled(); - Objects.requireNonNull(envName, "The name of the environment of interest needs to be provided."); - final List< String > cmd = new ArrayList<>( Arrays.asList( "create", "-p", getEnvDir(envName) ) ); - if (channels == null) channels = new ArrayList<>(); - for (String chan : channels) { cmd.add("-c"); cmd.add(chan);} - if (packages == null) packages = new ArrayList<>(); - cmd.addAll(packages); - if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); - runMamba(cmd.toArray(new String[0])); - } - /** * Returns Conda version as a {@code String}. * @@ -620,9 +539,9 @@ public String getVersion() throws IOException, InterruptedException { * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used + * @throws IllegalStateException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException + public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeException, IOException, InterruptedException { checkMambaInstalled(); Thread mainThread = Thread.currentThread(); @@ -730,9 +649,9 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE * If the current thread is interrupted by another thread while it * is waiting, then the wait is ended and an InterruptedException is * thrown. - * @throws MambaInstallException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used + * @throws IllegalStateException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException, MambaInstallException + public void runMamba(final String... args ) throws RuntimeException, IOException, InterruptedException { checkMambaInstalled(); runMamba(false, args); diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 79f88c1..c32371c 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -174,7 +174,7 @@ else if (!workDir.mkdirs()) { Mamba conda = new Mamba(Mamba.BASE_PATH); conda.installMicromamba(); conda.createWithYaml(envDir, environmentYaml.getAbsolutePath()); - } catch (InterruptedException | URISyntaxException | MambaInstallException e) { + } catch (InterruptedException | URISyntaxException e) { throw new IOException(e); } diff --git a/src/main/java/org/apposed/appose/mamba/MambaInstallException.java b/src/main/java/org/apposed/appose/mamba/MambaInstallException.java deleted file mode 100644 index d0dc3f8..0000000 --- a/src/main/java/org/apposed/appose/mamba/MambaInstallException.java +++ /dev/null @@ -1,56 +0,0 @@ -/*- - * #%L - * Appose: multi-language interprocess cooperation with shared memory. - * %% - * Copyright (C) 2023 - 2024 Appose developers. - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.apposed.appose.mamba; - -/** - * Exception to be thrown when Micromamba is not found in the wanted directory - * - * @author Carlos Javier Garcia Lopez de Haro - */ -public class MambaInstallException extends Exception { - - private static final long serialVersionUID = 1L; - - /** - * Constructs a new exception with the default detail message - */ - public MambaInstallException() { - super("Micromamba installation not found in the provided directory."); - } - - /** - * Constructs a new exception with the specified detail message - * @param message - * the detail message. - */ - public MambaInstallException(String message) { - super(message); - } - -} From bb87fec61d9259d62810fe44a77721d8d4e88396 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:18:58 -0500 Subject: [PATCH 095/120] Fix Mamba.downloadMicromamba exception handling --- .../java/org/apposed/appose/mamba/Mamba.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index f78fddd..d3e64ff 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -373,26 +373,32 @@ public void setErrorOutputConsumer(Consumer custom) { this.customErrorConsumer = custom; } - private File downloadMicromamba() throws IOException, URISyntaxException { + private File downloadMicromamba() throws IOException, InterruptedException, URISyntaxException { final File tempFile = File.createTempFile( "micromamba", ".tar.bz2" ); tempFile.deleteOnExit(); URL website = MambaInstallerUtils.redirectedURL(new URL(MICROMAMBA_URL)); long size = MambaInstallerUtils.getFileSize(website); Thread currentThread = Thread.currentThread(); + IOException[] ioe = {null}; + InterruptedException[] ie = {null}; Thread dwnldThread = new Thread(() -> { try ( ReadableByteChannel rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(tempFile) ) { new FileDownloader(rbc, fos).call(currentThread); - } catch (IOException | InterruptedException e) { - e.printStackTrace(); } + catch (IOException e) { ioe[0] = e; } + catch (InterruptedException e) { ie[0] = e; } }); dwnldThread.start(); - while (dwnldThread.isAlive()) + while (dwnldThread.isAlive()) { + Thread.sleep(20); // 50 FPS update rate this.mambaDnwldProgressConsumer.accept(((double) tempFile.length()) / ((double) size)); - if ((((double) tempFile.length()) / ((double) size)) < 1) + } + if (ioe[0] != null) throw ioe[0]; + if (ie[0] != null) throw ie[0]; + if ((((double) tempFile.length()) / size) < 1) throw new IOException("Error downloading micromamba from: " + MICROMAMBA_URL); return tempFile; } From 8df1818d1c4882d75dccfbdecc27edbc19907c67 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:31:25 -0500 Subject: [PATCH 096/120] Remove more unnecessary mamba code --- .../java/org/apposed/appose/mamba/Mamba.java | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index d3e64ff..f4425fe 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -108,10 +108,6 @@ class Mamba { *
*/ private final String rootdir; - /** - * Path to the folder that contains the directories - */ - private final String envsdir; /** * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. * @@ -167,10 +163,6 @@ class Mamba { * Path where Appose installs Micromamba by default */ final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); - /** - * Name of the folder inside the {@link #rootdir} that contains the different Conda environments created by the Appose Micromamba - */ - final public static String ENVS_NAME = "envs"; /** * URL from where Micromamba is downloaded to be installed */ @@ -290,7 +282,6 @@ public Mamba(final String rootdir) { else this.rootdir = rootdir; this.mambaCommand = new File(this.rootdir + MICROMAMBA_RELATIVE_PATH).getAbsolutePath(); - this.envsdir = Paths.get(rootdir, ENVS_NAME).toAbsolutePath().toString(); boolean filesExist = Files.notExists( Paths.get( mambaCommand ) ); if (!filesExist) return; @@ -412,8 +403,6 @@ private void decompressMicromamba(final File tempFile) throws IOException, Inter throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath() + ". Please try installing it in another directory."); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); - if (!(new File(envsdir)).isDirectory() && !new File(envsdir).mkdirs()) - throw new IOException("Failed to create Micromamba default envs directory " + envsdir); boolean executableSet = new File(mambaCommand).setExecutable(true); if (!executableSet) throw new IOException("Cannot set file as executable due to missing permissions, " @@ -434,14 +423,6 @@ public void installMicromamba() throws IOException, InterruptedException, URISyn if (isMambaInstalled()) return; decompressMicromamba(downloadMicromamba()); } - - public String getEnvsDir() { - return this.envsdir; - } - - public String getEnvDir(String envName) { - return Paths.get(getEnvsDir(), envName).toString(); - } /** * Returns {@code \{"cmd.exe", "/c"\}} for Windows and an empty list for @@ -461,9 +442,9 @@ private static List< String > getBaseCommand() /** * Run {@code conda update} in the specified environment. A list of packages to * update and extra parameters can be specified as {@code args}. - * - * @param envName - * The environment name to be used for the update command. + * + * @param envDir + * The directory within which the environment will be updated. * @param args * The list of packages to be updated and extra parameters as * {@code String...}. @@ -475,10 +456,10 @@ private static List< String > getBaseCommand() * thrown. * @throws IllegalStateException if Micromamba has not been installed, thus the instance of {@link Mamba} cannot be used */ - public void updateIn( final String envName, final String... args ) throws IOException, InterruptedException + public void updateIn( final File envDir, final String... args ) throws IOException, InterruptedException { checkMambaInstalled(); - final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "-p", getEnvDir(envName) ) ); + final List< String > cmd = new ArrayList<>( Arrays.asList( "update", "--prefix", envDir.getAbsolutePath() ) ); cmd.addAll( Arrays.asList( args ) ); if (!cmd.contains("--yes") && !cmd.contains("-y")) cmd.add("--yes"); runMamba(cmd.toArray(new String[0])); From 0d96b4f3e02b399dc7c364915b8fc18a5528b7e2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 30 Aug 2024 15:45:09 -0500 Subject: [PATCH 097/120] Clean up dependencies * Purge the barely-used Apache Commons Lang3. * Move Apache Commons I/O to test scope. * Make Apache Commons Compress available to Java-based workers. --- pom.xml | 18 +++++++----------- .../java/org/apposed/appose/Environment.java | 17 +++++++++-------- .../java/org/apposed/appose/mamba/Mamba.java | 18 ++++++++++-------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 719d553..0376c0d 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ @@ -108,7 +108,6 @@ org.apache.ivy ivy - ${ivy.version} @@ -121,14 +120,10 @@ jna-platform
- - - commons-io - commons-io - + org.apache.commons - commons-lang3 + commons-compress @@ -143,9 +138,10 @@ test - org.apache.commons - commons-compress - 1.24.0 + commons-io + commons-io + test + diff --git a/src/main/java/org/apposed/appose/Environment.java b/src/main/java/org/apposed/appose/Environment.java index d442a6e..3bbfcbd 100644 --- a/src/main/java/org/apposed/appose/Environment.java +++ b/src/main/java/org/apposed/appose/Environment.java @@ -127,14 +127,15 @@ default Service java(String mainClass, List classPath, // Ensure that the classpath includes Appose and its dependencies. // NB: This list must match Appose's dependencies in pom.xml! - List> apposeDeps = Arrays.asList(// - org.apposed.appose.GroovyWorker.class, // ------> org.apposed:appose - org.apache.groovy.util.ScriptRunner.class, // --> org.codehaus.groovy:groovy - groovy.json.JsonOutput.class, // ---------------> org.codehaus.groovy:groovy-json - org.apache.ivy.Ivy.class, // -------------------> org.apache.ivy:ivy - com.sun.jna.Pointer.class, // ------------------> com.sun.jna:jna - com.sun.jna.platform.linux.LibRT.class, // -----> com.sun.jna:jna-platform - com.sun.jna.platform.win32.Kernel32.class // ---> com.sun.jna:jna-platform + List> apposeDeps = Arrays.asList( + org.apposed.appose.GroovyWorker.class, // ------------------------> org.apposed:appose + org.apache.groovy.util.ScriptRunner.class, // --------------------> org.codehaus.groovy:groovy + groovy.json.JsonOutput.class, // ---------------------------------> org.codehaus.groovy:groovy-json + org.apache.ivy.Ivy.class, // -------------------------------------> org.apache.ivy:ivy + com.sun.jna.Pointer.class, // ------------------------------------> com.sun.jna:jna + com.sun.jna.platform.linux.LibRT.class, // -----------------------> com.sun.jna:jna-platform + com.sun.jna.platform.win32.Kernel32.class, // --------------------> com.sun.jna:jna-platform + org.apache.commons.compress.archivers.ArchiveException.class // --> org.apache.commons:commons-compress ); for (Class depClass : apposeDeps) { File location = FilePaths.location(depClass); diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index f4425fe..c3a67bc 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -59,8 +59,6 @@ package org.apposed.appose.mamba; -import org.apache.commons.lang3.SystemUtils; - import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -156,7 +154,7 @@ class Mamba { /** * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ - private final static String MICROMAMBA_RELATIVE_PATH = SystemUtils.IS_OS_WINDOWS ? + private final static String MICROMAMBA_RELATIVE_PATH = isWindowsOS() ? Paths.get("Library", "bin", "micromamba.exe").toString() : Paths.get("bin", "micromamba").toString(); /** @@ -434,7 +432,7 @@ public void installMicromamba() throws IOException, InterruptedException, URISyn private static List< String > getBaseCommand() { final List< String > cmd = new ArrayList<>(); - if ( SystemUtils.IS_OS_WINDOWS ) + if ( isWindowsOS() ) cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); return cmd; } @@ -500,7 +498,7 @@ public void createWithYaml( final File envDir, final String envYaml ) throws IOE */ public String getVersion() throws IOException, InterruptedException { final List< String > cmd = getBaseCommand(); - if (mambaCommand.contains(" ") && SystemUtils.IS_OS_WINDOWS) + if (mambaCommand.contains(" ") && isWindowsOS()) cmd.add( surroundWithQuotes(Arrays.asList( coverArgWithDoubleQuotes(mambaCommand), "--version" )) ); else cmd.addAll( Arrays.asList( coverArgWithDoubleQuotes(mambaCommand), "--version" ) ); @@ -538,12 +536,12 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE List argsList = new ArrayList<>(); argsList.add( coverArgWithDoubleQuotes(mambaCommand) ); argsList.addAll( Arrays.stream( args ).map(aa -> { - if (aa.contains(" ") && SystemUtils.IS_OS_WINDOWS) return coverArgWithDoubleQuotes(aa); + if (aa.contains(" ") && isWindowsOS()) return coverArgWithDoubleQuotes(aa); else return aa; }).collect(Collectors.toList()) ); boolean containsSpaces = argsList.stream().anyMatch(aa -> aa.contains(" ")); - if (!containsSpaces || !SystemUtils.IS_OS_WINDOWS) cmd.addAll(argsList); + if (!containsSpaces || !isWindowsOS()) cmd.addAll(argsList); else cmd.add(surroundWithQuotes(argsList)); ProcessBuilder builder = getBuilder(isInheritIO).command(cmd); @@ -656,7 +654,7 @@ private static String coverArgWithDoubleQuotes(String arg) { for (String schar : specialChars) { if (arg.startsWith("\"") && arg.endsWith("\"")) continue; - if (arg.contains(schar) && SystemUtils.IS_OS_WINDOWS) { + if (arg.contains(schar) && isWindowsOS()) { return "\"" + arg + "\""; } } @@ -679,4 +677,8 @@ private static String surroundWithQuotes(List args) { arg += "\""; return arg; } + + private static boolean isWindowsOS() { + return System.getProperty("os.name").startsWith("Windows"); + } } From 0d74f40917a501c2e2fc43418a92258b5d4827ed Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:19:19 -0500 Subject: [PATCH 098/120] Pass the Builder instance to BuildHandler#build That way, as the needed state to pass to each BuildHandler grows, it won't require API changes. In particular, access to event subscribers will be needed by BuildHandler plugins. --- src/main/java/org/apposed/appose/BuildHandler.java | 7 ++++--- src/main/java/org/apposed/appose/Builder.java | 6 ++++-- src/main/java/org/apposed/appose/mamba/MambaHandler.java | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/apposed/appose/BuildHandler.java b/src/main/java/org/apposed/appose/BuildHandler.java index 7315f53..f9cf42b 100644 --- a/src/main/java/org/apposed/appose/BuildHandler.java +++ b/src/main/java/org/apposed/appose/BuildHandler.java @@ -64,9 +64,10 @@ public interface BuildHandler { * Executes the environment build, according to the configured channels and includes. * * @param envDir The directory into which the environment will be built. - * @param config The table into which environment configuration will be recorded. - * @see Builder#build(String) + * @param builder The {@link Builder} instance managing the build process. + * Contains output configuration table. * @throws IOException If something goes wrong building the environment. + * @see Builder#build(String) */ - void build(File envDir, Map> config) throws IOException; + void build(File envDir, Builder builder) throws IOException; } diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index b10958f..c76e845 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -52,6 +52,8 @@ */ public class Builder { + public final Map> config = new HashMap<>(); + private final List handlers; private boolean includeSystemPath; @@ -276,8 +278,8 @@ public Environment build(File envDir) throws IOException { throw new IllegalArgumentException("Not a directory: " + envDir); } - Map> config = new HashMap<>(); - for (BuildHandler handler : handlers) handler.build(envDir, config); + config.clear(); + for (BuildHandler handler : handlers) handler.build(envDir, this); String base = envDir.getAbsolutePath(); diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index c32371c..5d041c7 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -29,6 +29,7 @@ package org.apposed.appose.mamba; import org.apposed.appose.BuildHandler; +import org.apposed.appose.Builder; import org.apposed.appose.FilePaths; import java.io.File; @@ -96,7 +97,7 @@ public String envName() { } @Override - public void build(File envDir, Map> config) throws IOException { + public void build(File envDir, Builder builder) throws IOException { if (!channels.isEmpty() || !condaIncludes.isEmpty() || !pypiIncludes.isEmpty()) { throw new UnsupportedOperationException( "Sorry, I don't know how to mix in additional packages from conda or PyPI yet." + @@ -104,7 +105,7 @@ public void build(File envDir, Map> config) throws IOExcept } if (yamlIncludes.isEmpty()) { // Nothing for this handler to do. - fillConfig(envDir, config); + fillConfig(envDir, builder.config); return; } if (yamlIncludes.size() > 1) { @@ -118,7 +119,7 @@ public void build(File envDir, Map> config) throws IOExcept if (new File(envDir, "conda-meta").isDirectory()) { // This environment has already been populated. // TODO: Should we update it? For now, we just use it. - fillConfig(envDir, config); + fillConfig(envDir, builder.config); return; } @@ -182,7 +183,7 @@ else if (!workDir.mkdirs()) { // at least the environment.yml file, and maybe other files from other handlers. FilePaths.moveDirectory(workDir, envDir, false); - fillConfig(envDir, config); + fillConfig(envDir, builder.config); } private List lines(String content) { From 1cf53e13ef907af6a75f26ea9b74c29211368f11 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:21:36 -0500 Subject: [PATCH 099/120] Make minor javadoc, comment, and exception tweaks --- src/main/java/org/apposed/appose/Builder.java | 8 +++--- .../java/org/apposed/appose/mamba/Mamba.java | 25 ++++++++++--------- .../apposed/appose/mamba/MambaHandler.java | 4 +-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index c76e845..57d6174 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -267,11 +267,11 @@ public Environment build(String envName) throws IOException { */ public Environment build(File envDir) throws IOException { if (envDir == null) { - throw new IllegalArgumentException("No environment base directory given."); + throw new IllegalArgumentException("No environment directory given."); } if (!envDir.exists()) { if (!envDir.mkdirs()) { - throw new RuntimeException("Failed to create environment base directory: " + envDir); + throw new RuntimeException("Failed to create environment directory: " + envDir); } } if (!envDir.isDirectory()) { @@ -287,7 +287,7 @@ public Environment build(File envDir) throws IOException { List binPaths = listFromConfig("binPaths", config); List classpath = listFromConfig("classpath", config); - // Always add environment directory itself to the binPaths. + // Always add the environment directory itself to the binPaths. // Especially important on Windows, where python.exe is not tucked into a bin subdirectory. binPaths.add(envDir.getAbsolutePath()); @@ -304,8 +304,6 @@ public Environment build(File envDir) throws IOException { }; } - // -- Helper methods -- - private boolean handle(Function handlerFunction) { boolean handled = false; for (BuildHandler handler : handlers) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index c3a67bc..b7033b8 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -108,11 +108,11 @@ class Mamba { private final String rootdir; /** * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. - * + * */ private Double mambaDnwldProgress = 0.0; /** - * Progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba + * Progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba * software. VAlue between 0 and 1. */ private Double mambaDecompressProgress = 0.0; @@ -312,38 +312,38 @@ private void checkMambaInstalled() { } /** - * + * * @return the progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. */ public double getMicromambaDownloadProgress(){ return this.mambaDnwldProgress; } - + /** - * - * @return the the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba + * + * @return the the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba * software. VAlue between 0 and 1. */ public double getMicromambaDecompressProgress(){ return this.mambaDecompressProgress; } - + /** - * + * * @return all the console output produced by micromamba ever since the {@link Mamba} was instantiated */ public String getMicromambaConsoleStream(){ return this.mambaConsoleOut; } - + /** - * + * * @return all the error output produced by micromamba ever since the {@link Mamba} was instantiated */ public String getMicromambaErrStream(){ return mambaConsoleErr; } - + /** * Set a custom consumer for the console output of every micromamba call * @param custom @@ -408,7 +408,8 @@ private void decompressMicromamba(final File tempFile) throws IOException, Inter } /** - * Install Micromamba automatically + * Downloads and installs Micromamba. + * * @throws IOException * If an I/O error occurs. * @throws InterruptedException diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 5d041c7..0b3b9a2 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -100,7 +100,7 @@ public String envName() { public void build(File envDir, Builder builder) throws IOException { if (!channels.isEmpty() || !condaIncludes.isEmpty() || !pypiIncludes.isEmpty()) { throw new UnsupportedOperationException( - "Sorry, I don't know how to mix in additional packages from conda or PyPI yet." + + "Sorry, I don't know how to mix in additional packages from Conda or PyPI yet." + " Please put them in your environment.yml for now."); } if (yamlIncludes.isEmpty()) { @@ -110,7 +110,7 @@ public void build(File envDir, Builder builder) throws IOException { } if (yamlIncludes.size() > 1) { throw new UnsupportedOperationException( - "Sorry, I can't synthesize micromamba environments from multiple environment.yml files yet." + + "Sorry, I can't synthesize Conda environments from multiple environment.yml files yet." + " Please use a single environment.yml for now."); } From fd0bdc45bcbd64b66a2505484888ec174c6f01f4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:22:18 -0500 Subject: [PATCH 100/120] Remove more unneeded Mamba code --- src/main/java/org/apposed/appose/mamba/Mamba.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index b7033b8..24a613c 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -280,14 +280,6 @@ public Mamba(final String rootdir) { else this.rootdir = rootdir; this.mambaCommand = new File(this.rootdir + MICROMAMBA_RELATIVE_PATH).getAbsolutePath(); - boolean filesExist = Files.notExists( Paths.get( mambaCommand ) ); - if (!filesExist) - return; - try { - getVersion(); - } catch (Exception ex) { - return; - } } /** From 01e9242d32f7b0c2da8007dfc23e5b047fc82650 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:22:56 -0500 Subject: [PATCH 101/120] Remove unused variable declarations --- src/test/java/org/apposed/appose/FilePathsTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apposed/appose/FilePathsTest.java b/src/test/java/org/apposed/appose/FilePathsTest.java index 24396fa..3b50a23 100644 --- a/src/test/java/org/apposed/appose/FilePathsTest.java +++ b/src/test/java/org/apposed/appose/FilePathsTest.java @@ -56,8 +56,8 @@ public void testFindExe() throws IOException { File tmpDir = Files.createTempDirectory("appose-FilePathsTest-testFindExe-").toFile(); try { // Set up some red herrings. - File walk = createStubFile(tmpDir, "walk"); - File fly = createStubFile(tmpDir, "fly"); + createStubFile(tmpDir, "walk"); + createStubFile(tmpDir, "fly"); File binDir = createDirectory(tmpDir, "bin"); File binFly = createStubFile(binDir, "fly"); // Mark the desired match as executable. @@ -103,7 +103,7 @@ public void testMoveDirectory() throws IOException { File dinnerFile2 = createStubFile(dinnerDir, "wine"); File destDir = createDirectory(tmpDir, "dest"); File destLunchDir = createDirectory(destDir, "lunch"); - File destLunchFile1 = createStubFile(destLunchDir, "apples", "gala"); + createStubFile(destLunchDir, "apples", "gala"); // Move the source directory to the destination. FilePaths.moveDirectory(srcDir, destDir, false); From e68bc8d1d0447274dff4187744e0afe0454d5396 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:23:51 -0500 Subject: [PATCH 102/120] Add build event subscription infrastructure There are three kinds of events possible: * Progress update on some action - e.g. a file download * Output message - e.g. the stdout stream of a micromamba invocation * Error message - e.g. the stderr stream of a micromamba invocation --- .../java/org/apposed/appose/BuildHandler.java | 18 ++++++++++- src/main/java/org/apposed/appose/Builder.java | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/BuildHandler.java b/src/main/java/org/apposed/appose/BuildHandler.java index f9cf42b..440bbe5 100644 --- a/src/main/java/org/apposed/appose/BuildHandler.java +++ b/src/main/java/org/apposed/appose/BuildHandler.java @@ -65,9 +65,25 @@ public interface BuildHandler { * * @param envDir The directory into which the environment will be built. * @param builder The {@link Builder} instance managing the build process. - * Contains output configuration table. + * Contains event subscribers and output configuration table. * @throws IOException If something goes wrong building the environment. * @see Builder#build(String) */ void build(File envDir, Builder builder) throws IOException; + + default void progress(Builder builder, String title, long current) { + progress(builder, title, current, -1); + } + + default void progress(Builder builder, String title, long current, long maximum) { + builder.progressSubscribers.forEach(subscriber -> subscriber.accept(title, current, maximum)); + } + + default void output(Builder builder, String message) { + builder.outputSubscribers.forEach(subscriber -> subscriber.accept(message)); + } + + default void error(Builder builder, String message) { + builder.errorSubscribers.forEach(subscriber -> subscriber.accept(message)); + } } diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 57d6174..ed339fc 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Objects; import java.util.ServiceLoader; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -53,6 +54,9 @@ public class Builder { public final Map> config = new HashMap<>(); + public final List progressSubscribers = new ArrayList<>(); + public final List> outputSubscribers = new ArrayList<>(); + public final List> errorSubscribers = new ArrayList<>(); private final List handlers; @@ -64,6 +68,28 @@ public class Builder { ServiceLoader.load(BuildHandler.class).forEach(handlers::add); } + /** + * Registers a callback method to be invoked when progress happens during environment building. + * + * @param subscriber Party to inform when build progress happens. + * @return This {@code Builder} instance, for fluent-style programming. + * @see ProgressConsumer#accept + */ + public Builder subscribeProgress(ProgressConsumer subscriber) { + progressSubscribers.add(subscriber); + return this; + } + + public Builder subscribeOutput(Consumer subscriber) { + outputSubscribers.add(subscriber); + return this; + } + + public Builder subscribeError(Consumer subscriber) { + errorSubscribers.add(subscriber); + return this; + } + /** * TODO * @@ -315,4 +341,8 @@ private static List listFromConfig(String key, Map> List value = config.getOrDefault(key, Collections.emptyList()); return value.stream().map(Object::toString).collect(Collectors.toList()); } + + public interface ProgressConsumer { + void accept(String title, long current, long maximum); + } } From 211319a619d35a161d36c23c2a76fef4101ee1a3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:44:57 -0500 Subject: [PATCH 103/120] Simplify Mamba event consumer infrastructure --- .../java/org/apposed/appose/mamba/Mamba.java | 150 +++++------------- 1 file changed, 44 insertions(+), 106 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 24a613c..0485a8d 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -77,6 +77,7 @@ import java.util.Calendar; import java.util.List; import java.util.UUID; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -106,51 +107,23 @@ class Mamba { * */ private final String rootdir; - /** - * Progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. - * - */ - private Double mambaDnwldProgress = 0.0; - /** - * Progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba - * software. VAlue between 0 and 1. - */ - private Double mambaDecompressProgress = 0.0; + /** * Consumer that tracks the progress in the download of micromamba, the software used - * by this class to manage Conda environments - */ - private final Consumer mambaDnwldProgressConsumer = this::updateMambaDnwldProgress; - /** - * Consumer that tracks the progress decompressing the downloaded micromamba files. - */ - private final Consumer mambaDecompressProgressConsumer = this::updateMambaDecompressProgress; - /** - * String that contains all the console output produced by micromamba ever since the {@link Mamba} was instantiated - */ - private String mambaConsoleOut = ""; - /** - * String that contains all the error output produced by micromamba ever since the {@link Mamba} was instantiated - */ - private String mambaConsoleErr = ""; - /** - * User custom consumer that tracks the console output produced by the micromamba process when it is executed. + * by this class to manage Conda environments. */ - private Consumer customConsoleConsumer; - /** - * User custom consumer that tracks the error output produced by the micromamba process when it is executed. - */ - private Consumer customErrorConsumer; + private BiConsumer mambaDownloadProgressConsumer; + /** - * Consumer that tracks the console output produced by the micromamba process when it is executed. - * This consumer saves all the log of every micromamba execution + * Consumer that tracks the standard output stream produced by the micromamba process when it is executed. */ - private final Consumer consoleConsumer = this::updateConsoleConsumer; + private Consumer outputConsumer; + /** - * Consumer that tracks the error output produced by the micromamba process when it is executed. - * This consumer saves all the log of every micromamba execution + * Consumer that tracks the standard error stream produced by the micromamba process when it is executed. */ - private final Consumer errConsumer = this::updateErrorConsumer; + private Consumer errorConsumer; + /** * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ @@ -189,29 +162,20 @@ private static String microMambaPlatform() { default: return null; } } - - private void updateMambaDnwldProgress(Double pp) { - double progress = pp != null ? pp : 0.0; - mambaDnwldProgress = progress * 1.0; + + private void updateMambaDownloadProgress(long current, long total) { + if (mambaDownloadProgressConsumer != null) + mambaDownloadProgressConsumer.accept(current, total); } - - private void updateConsoleConsumer(String str) { - if (str == null) str = ""; - mambaConsoleOut += str; - if (customConsoleConsumer != null) - customConsoleConsumer.accept(str); + + private void updateOutputConsumer(String str) { + if (outputConsumer != null) + outputConsumer.accept(str == null ? "" : str); } - + private void updateErrorConsumer(String str) { - if (str == null) str = ""; - mambaConsoleErr += str; - if (customErrorConsumer != null) - customErrorConsumer.accept(str); - } - - private void updateMambaDecompressProgress(Double pp) { - double progress = pp != null ? pp : 0.0; - this.mambaDecompressProgress = progress * 1.0; + if (errorConsumer != null) + errorConsumer.accept(str == null ? "" : str); } /** @@ -304,56 +268,32 @@ private void checkMambaInstalled() { } /** - * - * @return the progress made on the download from the Internet of the micromamba software. VAlue between 0 and 1. + * Registers the consumer for the standard error stream of every micromamba call. + * @param consumer + * callback function invoked for each stderr line of every micromamba call */ - public double getMicromambaDownloadProgress(){ - return this.mambaDnwldProgress; + public void setMambaDownloadProgressConsumer(BiConsumer consumer) { + this.mambaDownloadProgressConsumer = consumer; } /** - * - * @return the the progress made on the decompressing the micromamba files downloaded from the Internet of the micromamba - * software. VAlue between 0 and 1. + * Registers the consumer for the standard output stream of every micromamba call. + * @param consumer + * callback function invoked for each stdout line of every micromamba call */ - public double getMicromambaDecompressProgress(){ - return this.mambaDecompressProgress; + public void setOutputConsumer(Consumer consumer) { + this.outputConsumer = consumer; } /** - * - * @return all the console output produced by micromamba ever since the {@link Mamba} was instantiated + * Registers the consumer for the standard error stream of every micromamba call. + * @param consumer + * callback function invoked for each stderr line of every micromamba call */ - public String getMicromambaConsoleStream(){ - return this.mambaConsoleOut; + public void setErrorConsumer(Consumer consumer) { + this.errorConsumer = consumer; } - /** - * - * @return all the error output produced by micromamba ever since the {@link Mamba} was instantiated - */ - public String getMicromambaErrStream(){ - return mambaConsoleErr; - } - - /** - * Set a custom consumer for the console output of every micromamba call - * @param custom - * custom consumer that receives every console line outputed by ecery micromamba call - */ - public void setConsoleOutputConsumer(Consumer custom) { - this.customConsoleConsumer = custom; - } - - /** - * Set a custom consumer for the error output of every micromamba call - * @param custom - * custom consumer that receives every error line outputed by ecery micromamba call - */ - public void setErrorOutputConsumer(Consumer custom) { - this.customErrorConsumer = custom; - } - private File downloadMicromamba() throws IOException, InterruptedException, URISyntaxException { final File tempFile = File.createTempFile( "micromamba", ".tar.bz2" ); tempFile.deleteOnExit(); @@ -375,11 +315,11 @@ private File downloadMicromamba() throws IOException, InterruptedException, URIS dwnldThread.start(); while (dwnldThread.isAlive()) { Thread.sleep(20); // 50 FPS update rate - this.mambaDnwldProgressConsumer.accept(((double) tempFile.length()) / ((double) size)); + updateMambaDownloadProgress(tempFile.length(), size); } if (ioe[0] != null) throw ioe[0]; if (ie[0] != null) throw ie[0]; - if ((((double) tempFile.length()) / size) < 1) + if (tempFile.length() < size) throw new IOException("Error downloading micromamba from: " + MICROMAMBA_URL); return tempFile; } @@ -540,7 +480,7 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE ProcessBuilder builder = getBuilder(isInheritIO).command(cmd); Process process = builder.start(); // Use separate threads to read each stream to avoid a deadlock. - this.consoleConsumer.accept(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); + updateOutputConsumer(sdf.format(Calendar.getInstance().getTime()) + " -- STARTING INSTALLATION" + System.lineSeparator()); long updatePeriod = 300; Thread outputThread = new Thread(() -> { try ( @@ -577,7 +517,7 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE // Sleep for a bit to avoid busy waiting Thread.sleep(60); if (System.currentTimeMillis() - t0 > updatePeriod) { - this.consoleConsumer.accept(processChunk); + updateOutputConsumer(processChunk); processChunk = ""; errChunk = ""; t0 = System.currentTimeMillis(); @@ -591,8 +531,8 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE errBuff.append(new String(buffer, 0, errStream.read(buffer))); errChunk += ERR_STREAM_UUUID + errBuff.toString().trim(); } - this.errConsumer.accept(errChunk); - this.consoleConsumer.accept(processChunk + System.lineSeparator() + updateErrorConsumer(errChunk); + updateOutputConsumer(processChunk + System.lineSeparator() + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED PROCESS"); } catch (IOException | InterruptedException e) { e.printStackTrace(); @@ -609,9 +549,7 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE // Wait for all output to be read outputThread.join(); if (processResult != 0) - throw new RuntimeException("Error executing the following command: " + builder.command() - + System.lineSeparator() + this.mambaConsoleOut - + System.lineSeparator() + this.mambaConsoleErr); + throw new RuntimeException("Exit code " + processResult + " from command execution: " + builder.command()); } /** From d7130c2b01a0256dd6eda5974d12f96f9f300300 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:45:38 -0500 Subject: [PATCH 104/120] Remove unneeded import --- src/main/java/org/apposed/appose/mamba/Mamba.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 0485a8d..4bcb201 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -69,7 +69,6 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.nio.file.Files; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; From 71ba68cebbd62dec537da606c21dad7950fbd8a4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:45:49 -0500 Subject: [PATCH 105/120] Fix some whitespace issues --- src/main/java/org/apposed/appose/mamba/Mamba.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 4bcb201..e39ea47 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -87,11 +87,12 @@ * @author Carlos Garcia */ class Mamba { - + /** * String containing the path that points to the micromamba executable */ final String mambaCommand; + /** * Root directory of micromamba that also contains the environments folder * @@ -129,15 +130,18 @@ class Mamba { private final static String MICROMAMBA_RELATIVE_PATH = isWindowsOS() ? Paths.get("Library", "bin", "micromamba.exe").toString() : Paths.get("bin", "micromamba").toString(); + /** * Path where Appose installs Micromamba by default */ final public static String BASE_PATH = Paths.get(System.getProperty("user.home"), ".local", "share", "appose", "micromamba").toString(); + /** * URL from where Micromamba is downloaded to be installed */ public final static String MICROMAMBA_URL = "https://micro.mamba.pm/api/micromamba/" + microMambaPlatform() + "/latest"; + /** * ID used to identify the text retrieved from the error stream when a consumer is used */ @@ -322,22 +326,23 @@ private File downloadMicromamba() throws IOException, InterruptedException, URIS throw new IOException("Error downloading micromamba from: " + MICROMAMBA_URL); return tempFile; } - + private void decompressMicromamba(final File tempFile) throws IOException, InterruptedException { final File tempTarFile = File.createTempFile( "micromamba", ".tar" ); tempTarFile.deleteOnExit(); MambaInstallerUtils.unBZip2(tempFile, tempTarFile); File mambaBaseDir = new File(rootdir); if (!mambaBaseDir.isDirectory() && !mambaBaseDir.mkdirs()) - throw new IOException("Failed to create Micromamba default directory " + mambaBaseDir.getParentFile().getAbsolutePath() - + ". Please try installing it in another directory."); + throw new IOException("Failed to create Micromamba default directory " + + mambaBaseDir.getParentFile().getAbsolutePath() + + ". Please try installing it in another directory."); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); boolean executableSet = new File(mambaCommand).setExecutable(true); if (!executableSet) throw new IOException("Cannot set file as executable due to missing permissions, " + "please do it manually: " + mambaCommand); } - + /** * Downloads and installs Micromamba. * From 8a19094ba1592d8a7cb9786827acb217874fc579 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2024 17:51:50 -0500 Subject: [PATCH 106/120] Forward Mamba events to Builder event subscribers --- src/main/java/org/apposed/appose/mamba/MambaHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 0b3b9a2..7775a95 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -173,6 +173,12 @@ else if (!workDir.mkdirs()) { // Finally, we can build the environment from the environment.yml file. try { Mamba conda = new Mamba(Mamba.BASE_PATH); + conda.setOutputConsumer(msg -> builder.outputSubscribers.forEach(sub -> sub.accept(msg))); + conda.setErrorConsumer(msg -> builder.errorSubscribers.forEach(sub -> sub.accept(msg))); + conda.setMambaDownloadProgressConsumer((cur, max) -> { + builder.progressSubscribers.forEach(subscriber -> subscriber.accept("Downloading micromamba", cur, max)); + }); + conda.installMicromamba(); conda.createWithYaml(envDir, environmentYaml.getAbsolutePath()); } catch (InterruptedException | URISyntaxException e) { From 2ca4fd6d414d52ec370606062403c8f20211608a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Sep 2024 12:46:01 -0500 Subject: [PATCH 107/120] CI: run the build on all three major platforms --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b005b9c..1f08329 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,12 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v2 From 8a9a224a160ea03e92d956dd3e12a112470511f5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Sep 2024 12:46:22 -0500 Subject: [PATCH 108/120] CI: upgrade checkout and setup-java versions --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f08329..c67f2eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,10 +20,10 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'zulu' From 004ef260db2f43f461d947900fec5e3d1dcd2132 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Sep 2024 12:46:54 -0500 Subject: [PATCH 109/120] CI: cache the Appose cache So that every single build doesn't need to download and unpack micromamba, then download and unpack all conda environment packages. --- .github/workflows/build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c67f2eb..9819cdd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,15 @@ jobs: with: python-version: '3.10' + - name: Cache Appose environments + id: cache-appose + uses: actions/cache@v4 + with: + path: ~/.local/share/appose + key: ${{ runner.os }}-build-appose-${{ hashFiles('*') }} + restore-keys: | + ${{ runner.os }}-build-appose- + - name: Set up CI environment run: .github/setup.sh From f3a8a3e26fb793afe4e392279d52150026eca20a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Sep 2024 16:29:53 -0500 Subject: [PATCH 110/120] Improve ApposeTest debugging features --- .../java/org/apposed/appose/ApposeTest.java | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index d5cf6e5..03cc8f6 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -73,7 +73,7 @@ public class ApposeTest { public void testGroovy() throws IOException, InterruptedException { Environment env = Appose.system(); try (Service service = env.groovy()) { - //service.debug(System.err::println); + maybeDebug(service); executeAndAssert(service, COLLATZ_GROOVY); } } @@ -82,7 +82,7 @@ public void testGroovy() throws IOException, InterruptedException { public void testPython() throws IOException, InterruptedException { Environment env = Appose.system(); try (Service service = env.python()) { - //service.debug(System.err::println); + maybeDebug(service); executeAndAssert(service, COLLATZ_PYTHON); } } @@ -91,13 +91,13 @@ public void testPython() throws IOException, InterruptedException { public void testConda() throws IOException, InterruptedException { Environment env = Appose.conda(new File("src/test/resources/envs/cowsay.yml")).build(); try (Service service = env.python()) { - //service.debug(System.err::println); + maybeDebug(service); Task task = service.task( "import cowsay\n" + "task.outputs['moo'] = cowsay.get_output_string('cow', 'moo')\n" ); task.waitFor(); - assertEquals(TaskStatus.COMPLETE, task.status); + assertComplete(task); String expectedMoo = " ___\n" + "| moo |\n" + @@ -156,9 +156,10 @@ class TaskState { // Wait for task to finish. task.waitFor(); + assertComplete(task); // Validate the execution result. - assertSame(TaskStatus.COMPLETE, task.status); + assertComplete(task); Number result = (Number) task.outputs.get("result"); assertEquals(91, result.intValue()); @@ -184,10 +185,34 @@ class TaskState { } TaskState completion = events.get(92); assertSame(ResponseType.COMPLETION, completion.responseType); - assertSame(TaskStatus.COMPLETE, completion.status); assertEquals("[90] -> 1", completion.message); assertEquals(90, completion.current); assertEquals(1, completion.maximum); assertNull(completion.error); } + + private void maybeDebug(Service service) { + String debug1 = System.getenv("DEBUG"); + String debug2 = System.getProperty("appose.debug"); + if (falsy(debug1) && falsy(debug2)) return; + service.debug(System.err::println); + } + + private boolean falsy(String value) { + if (value == null) return true; + String tValue = value.trim(); + if (tValue.isEmpty()) return true; + if (tValue.equalsIgnoreCase("false")) return true; + if (tValue.equals("0")) return true; + return false; + } + private void assertComplete(Task task) { + String errorMessage = ""; + if (task.status != TaskStatus.COMPLETE) { + String caller = new RuntimeException().getStackTrace()[1].getMethodName(); + errorMessage = "TASK ERROR in method " + caller + ":\n" + task.error; + System.err.println(); + } + assertEquals(TaskStatus.COMPLETE, task.status, errorMessage); + } } From b53c524edccffc7bb0c1d7f23a1bd6981382f81f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Sep 2024 17:35:31 -0500 Subject: [PATCH 111/120] Increase ApposeTest#testServiceStartupFailure info Yeah, I know it's failing because it's using the system python on the system path. I guess ProcessBuilder does that, similar to a shell. But I wanted to add the debug output just to be friendly and clear. The question is: how do we configure ProcessBuilder not to do that? --- .../java/org/apposed/appose/ApposeTest.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 03cc8f6..1f6d6e5 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -115,10 +115,23 @@ public void testConda() throws IOException, InterruptedException { } @Test - public void testServiceStartupFailure() throws IOException { + public void testServiceStartupFailure() throws IOException, InterruptedException { Environment env = Appose.build("no-pythons-to-be-found-here"); try (Service service = env.python()) { - fail("Python worker process started successfully!?"); + String info = ""; + try { + Task task = service.task( + "import sys\n" + + "task.outputs['executable'] = sys.executable\n" + + "task.outputs['version'] = sys.version" + ); + task.waitFor(); + info += "\n- sys.executable = " + task.outputs.get("executable"); + info += "\n- sys.version = " + task.outputs.get("version"); + } + finally { + fail("Python worker process started successfully!?" + info); + } } catch (IllegalArgumentException exc) { assertEquals( From 7588ac00d8c842260fdf829bc549bd7a4607b3da Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Sep 2024 18:56:33 -0500 Subject: [PATCH 112/120] Fix bugs in updated micromamba build handler --- .../java/org/apposed/appose/Environment.java | 21 ++++++++++++++----- .../java/org/apposed/appose/mamba/Mamba.java | 19 ++++++++++------- .../apposed/appose/mamba/MambaHandler.java | 6 +++--- .../java/org/apposed/appose/ApposeTest.java | 2 +- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/apposed/appose/Environment.java b/src/main/java/org/apposed/appose/Environment.java index 3bbfcbd..4313939 100644 --- a/src/main/java/org/apposed/appose/Environment.java +++ b/src/main/java/org/apposed/appose/Environment.java @@ -183,13 +183,24 @@ default Service service(List exes, String... args) throws IOException { // Discern path to executable by searching the environment's binPaths. File exeFile = FilePaths.findExe(binPaths(), exes); - // If exeFile is null, just use the first executable bare, because there - // are scenarios like `pixi run python` where the intended executable will - // only by part of the system path while within the activated environment. - String exe = exeFile == null ? exes.get(0) : exeFile.getCanonicalPath(); + + // Calculate exe string. + List launchArgs = launchArgs(); + final String exe; + if (exeFile == null) { + if (launchArgs.isEmpty()) { + throw new IllegalArgumentException("No executables found amongst candidates: " + exes); + } + // No exeFile was found in the binPaths, but there are prefixed launchArgs. + // So we now try to use the first executable bare, because in this scenario + // we may have a situation like `pixi run python` where the intended executable + // become available on the system path while the environment is activated. + exe = exes.get(0); + } + else exe = exeFile.getCanonicalPath(); // Construct final args list: launchArgs + exe + args - List allArgs = new ArrayList<>(launchArgs()); + List allArgs = new ArrayList<>(launchArgs); allArgs.add(exe); allArgs.addAll(Arrays.asList(args)); diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index e39ea47..9cb2cf3 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -69,6 +69,7 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -127,9 +128,9 @@ class Mamba { /** * Relative path to the micromamba executable from the micromamba {@link #rootdir} */ - private final static String MICROMAMBA_RELATIVE_PATH = isWindowsOS() ? - Paths.get("Library", "bin", "micromamba.exe").toString() : - Paths.get("bin", "micromamba").toString(); + private final static Path MICROMAMBA_RELATIVE_PATH = isWindowsOS() ? + Paths.get("Library", "bin", "micromamba.exe") : + Paths.get("bin", "micromamba"); /** * Path where Appose installs Micromamba by default @@ -246,7 +247,7 @@ public Mamba(final String rootdir) { this.rootdir = BASE_PATH; else this.rootdir = rootdir; - this.mambaCommand = new File(this.rootdir + MICROMAMBA_RELATIVE_PATH).getAbsolutePath(); + this.mambaCommand = Paths.get(this.rootdir).resolve(MICROMAMBA_RELATIVE_PATH).toAbsolutePath().toString(); } /** @@ -337,10 +338,14 @@ private void decompressMicromamba(final File tempFile) throws IOException, Inter mambaBaseDir.getParentFile().getAbsolutePath() + ". Please try installing it in another directory."); MambaInstallerUtils.unTar(tempTarFile, mambaBaseDir); - boolean executableSet = new File(mambaCommand).setExecutable(true); - if (!executableSet) - throw new IOException("Cannot set file as executable due to missing permissions, " + File mmFile = new File(mambaCommand); + if (!mmFile.exists()) throw new IOException("Expected micromamba binary is missing: " + mambaCommand); + if (!mmFile.canExecute()) { + boolean executableSet = new File(mambaCommand).setExecutable(true); + if (!executableSet) + throw new IOException("Cannot set file as executable due to missing permissions, " + "please do it manually: " + mambaCommand); + } } /** diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 7775a95..9d5f52d 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -200,11 +200,11 @@ private List lines(String content) { } public void fillConfig(File envDir, Map> config) { - // If ${envDir}/bin directory exists, add it to binDirs. + // If ${envDir}/bin directory exists, add it to binPaths. File binDir = new File(envDir, "bin"); if (binDir.isDirectory()) { - config.computeIfAbsent("binDirs", k -> new ArrayList<>()); - config.get("binDirs").add(binDir.getAbsolutePath()); + config.computeIfAbsent("binPaths", k -> new ArrayList<>()); + config.get("binPaths").add(binDir.getAbsolutePath()); } } } diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 1f6d6e5..20b11eb 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -136,7 +136,7 @@ public void testServiceStartupFailure() throws IOException, InterruptedException catch (IllegalArgumentException exc) { assertEquals( "No executables found amongst candidates: " + - "[python, python3, python.exe, bin/python, bin/python.exe]", + "[python, python3, python.exe]", exc.getMessage() ); } From 5e5455ff2584f431d94fcd03b5d9ee856b73c81c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 20 Sep 2024 14:05:14 -0500 Subject: [PATCH 113/120] Declare thrown exceptions in consistent order --- src/test/java/org/apposed/appose/ApposeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 20b11eb..39469bd 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -143,7 +143,7 @@ public void testServiceStartupFailure() throws IOException, InterruptedException } public void executeAndAssert(Service service, String script) - throws InterruptedException, IOException + throws IOException, InterruptedException { Task task = service.task(script); From 89d6f6cd837675dc4e0022ebd5fc3e798067d83e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 20 Sep 2024 14:45:11 -0500 Subject: [PATCH 114/120] Use `mamba run -p $envDir ...` to run via mamba This sidesteps the thorny issue of environment activation within the same process, in favor of micromamba activating+running the desired worker launch command within the correct environment prefix, such that activation scripts get sourced properly beforehand. --- .../apposed/appose/mamba/MambaHandler.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 9d5f52d..906895f 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -103,9 +103,12 @@ public void build(File envDir, Builder builder) throws IOException { "Sorry, I don't know how to mix in additional packages from Conda or PyPI yet." + " Please put them in your environment.yml for now."); } + + Mamba conda = new Mamba(Mamba.BASE_PATH); + if (yamlIncludes.isEmpty()) { // Nothing for this handler to do. - fillConfig(envDir, builder.config); + fillConfig(conda, envDir, builder.config); return; } if (yamlIncludes.size() > 1) { @@ -119,7 +122,7 @@ public void build(File envDir, Builder builder) throws IOException { if (new File(envDir, "conda-meta").isDirectory()) { // This environment has already been populated. // TODO: Should we update it? For now, we just use it. - fillConfig(envDir, builder.config); + fillConfig(conda, envDir, builder.config); return; } @@ -172,7 +175,6 @@ else if (!workDir.mkdirs()) { // Finally, we can build the environment from the environment.yml file. try { - Mamba conda = new Mamba(Mamba.BASE_PATH); conda.setOutputConsumer(msg -> builder.outputSubscribers.forEach(sub -> sub.accept(msg))); conda.setErrorConsumer(msg -> builder.errorSubscribers.forEach(sub -> sub.accept(msg))); conda.setMambaDownloadProgressConsumer((cur, max) -> { @@ -181,15 +183,16 @@ else if (!workDir.mkdirs()) { conda.installMicromamba(); conda.createWithYaml(envDir, environmentYaml.getAbsolutePath()); - } catch (InterruptedException | URISyntaxException e) { + fillConfig(conda, envDir, builder.config); + } + catch (InterruptedException | URISyntaxException e) { throw new IOException(e); } - - // Lastly, we merge the contents of workDir into envDir. This will be - // at least the environment.yml file, and maybe other files from other handlers. - FilePaths.moveDirectory(workDir, envDir, false); - - fillConfig(envDir, builder.config); + finally { + // Lastly, we merge the contents of workDir into envDir. This will be + // at least the environment.yml file, and maybe other files from other handlers. + FilePaths.moveDirectory(workDir, envDir, false); + } } private List lines(String content) { @@ -199,12 +202,9 @@ private List lines(String content) { .collect(Collectors.toList()); } - public void fillConfig(File envDir, Map> config) { - // If ${envDir}/bin directory exists, add it to binPaths. - File binDir = new File(envDir, "bin"); - if (binDir.isDirectory()) { - config.computeIfAbsent("binPaths", k -> new ArrayList<>()); - config.get("binPaths").add(binDir.getAbsolutePath()); - } + private void fillConfig(Mamba conda, File envDir, Map> config) { + // Use `mamba run -p $envDir ...` to run within this environment. + config.computeIfAbsent("launchArgs", k -> new ArrayList<>()); + config.get("launchArgs").addAll(Arrays.asList(conda.mambaCommand, "run", "-p", envDir.getAbsolutePath())); } } From 60cb726fa9da2895f408de2545e5ec2ef82a3c5b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 20 Sep 2024 16:24:25 -0500 Subject: [PATCH 115/120] Add builder method for debugging env building Just a succinct shorthand for subscribing to progress, stdout, and stderr, printing all the events coming in. --- src/main/java/org/apposed/appose/Builder.java | 16 ++++++++++++++++ src/test/java/org/apposed/appose/ApposeTest.java | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index ed339fc..6b4f2ce 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -90,6 +90,22 @@ public Builder subscribeError(Consumer subscriber) { return this; } + /** + * Shorthand for {@link #subscribeProgress}, {@link #subscribeOutput}, + * and {@link #subscribeError} calls registering subscribers that + * emit their arguments to stdout. Useful for debugging environment + * construction, e.g. complex environments with many conda packages. + * + * @return This {@code Builder} instance, for fluent-style programming. + */ + public Builder logDebug() { + return subscribeProgress( + (title, cur, max) -> System.out.printf("%s: %d/%d\n", title, cur, max) + ) + .subscribeOutput(msg -> System.out.printf("[stdout] %s\n", msg)) + .subscribeError(msg -> System.out.printf("[stderr] %s\n", msg)); + } + /** * TODO * diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 39469bd..180295d 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -89,7 +89,10 @@ public void testPython() throws IOException, InterruptedException { @Test public void testConda() throws IOException, InterruptedException { - Environment env = Appose.conda(new File("src/test/resources/envs/cowsay.yml")).build(); + Environment env = Appose + .conda(new File("src/test/resources/envs/cowsay.yml")) + .logDebug() + .build(); try (Service service = env.python()) { maybeDebug(service); Task task = service.task( From 8b3bad607d74b7948f3f81ae333b48e0dd09e8a0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 20 Sep 2024 16:29:50 -0500 Subject: [PATCH 116/120] Remove unneeded TODO notes in shm logic --- .../java/org/apposed/appose/shm/ShmLinux.java | 1 - .../java/org/apposed/appose/shm/ShmMacOS.java | 1 - .../org/apposed/appose/shm/ShmWindows.java | 20 ++----------------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/apposed/appose/shm/ShmLinux.java b/src/main/java/org/apposed/appose/shm/ShmLinux.java index 85f021d..ac5e06f 100644 --- a/src/main/java/org/apposed/appose/shm/ShmLinux.java +++ b/src/main/java/org/apposed/appose/shm/ShmLinux.java @@ -155,7 +155,6 @@ private static long getSHMSize(final int shmFd) { final long size = LibRtOrC.lseek(shmFd, 0, LibRtOrC.SEEK_END); if (size == -1) { - // TODO remove LibRtOrC.close(shmFd); throw new RuntimeException("Failed to get shared memory segment size. Errno: " + Native.getLastError()); } return size; diff --git a/src/main/java/org/apposed/appose/shm/ShmMacOS.java b/src/main/java/org/apposed/appose/shm/ShmMacOS.java index bb4d405..388a2cc 100644 --- a/src/main/java/org/apposed/appose/shm/ShmMacOS.java +++ b/src/main/java/org/apposed/appose/shm/ShmMacOS.java @@ -146,7 +146,6 @@ private static long getSHMSize(final int shmFd) { final long size = MacosHelpers.INSTANCE.get_shared_memory_size(shmFd); if (size == -1) { - // TODO remove macosInstance.unlink_shared_memory(null);; throw new RuntimeException("Failed to get shared memory segment size. Errno: " + Native.getLastError()); } return size; diff --git a/src/main/java/org/apposed/appose/shm/ShmWindows.java b/src/main/java/org/apposed/appose/shm/ShmWindows.java index b52c91e..c558d4a 100644 --- a/src/main/java/org/apposed/appose/shm/ShmWindows.java +++ b/src/main/java/org/apposed/appose/shm/ShmWindows.java @@ -42,9 +42,6 @@ /** * Windows-specific shared memory implementation. - *

- * TODO separate unlink and close - *

* * @author Carlos Garcia Lopez de Haro * @author Tobias Pietzsch @@ -85,7 +82,7 @@ private static ShmInfo prepareShm(String name, boolean create, int prevSize = getSHMSize(shm_name); } while (prevSize >= 0); } else { - shm_name = nameMangle_TODO(name); + shm_name = name; prevSize = getSHMSize(shm_name); } ShmUtils.checkSize(shm_name, prevSize, size); @@ -126,7 +123,7 @@ private static ShmInfo prepareShm(String name, boolean create, int ShmInfo info = new ShmInfo<>(); info.size = shm_size; - info.name = nameUnmangle_TODO(shm_name); + info.name = shm_name; info.pointer = pointer; info.writePointer = writePointer; info.handle = hMapFile; @@ -134,19 +131,6 @@ private static ShmInfo prepareShm(String name, boolean create, int return info; } - // TODO equivalent of removing slash - private static String nameUnmangle_TODO (String memoryName){ - return memoryName; - } - - // TODO equivalent of adding slash - // Do we need the "Local\" prefix? - private static String nameMangle_TODO (String memoryName){ - // if (!memoryName.startsWith("Local" + File.separator) && !memoryName.startsWith("Global" + File.separator)) - // memoryName = "Local" + File.separator + memoryName; - return memoryName; - } - // name is WITH prefix etc private static boolean checkSHMExists ( final String name){ final WinNT.HANDLE hMapFile = Kernel32.INSTANCE.OpenFileMapping(WinNT.FILE_MAP_READ, false, name); From eced85c26aef452c1b9b39d17fbd8a01ca2c496f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 23 Sep 2024 14:11:57 -0500 Subject: [PATCH 117/120] Fix non-conda Appose environment behavior Only inject micromamba config (`mamba run -f ...`) launch args if the Appose environment is a conda one. And clean up the no-pythons-to-be-found-here directory on exit. This fixes the ApposeTest#testServiceStartupFailure. --- src/main/java/org/apposed/appose/mamba/MambaHandler.java | 9 +++++++-- src/test/java/org/apposed/appose/ApposeTest.java | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apposed/appose/mamba/MambaHandler.java b/src/main/java/org/apposed/appose/mamba/MambaHandler.java index 906895f..5cb6979 100644 --- a/src/main/java/org/apposed/appose/mamba/MambaHandler.java +++ b/src/main/java/org/apposed/appose/mamba/MambaHandler.java @@ -105,10 +105,15 @@ public void build(File envDir, Builder builder) throws IOException { } Mamba conda = new Mamba(Mamba.BASE_PATH); + boolean isCondaDir = new File(envDir, "conda-meta").isDirectory(); if (yamlIncludes.isEmpty()) { // Nothing for this handler to do. - fillConfig(conda, envDir, builder.config); + if (isCondaDir) { + // If directory already exists and is a conda environment prefix, + // inject needed micromamba stuff into the configuration. + fillConfig(conda, envDir, builder.config); + } return; } if (yamlIncludes.size() > 1) { @@ -119,7 +124,7 @@ public void build(File envDir, Builder builder) throws IOException { // Is this envDir an already-existing conda directory? // If so, we can update it. - if (new File(envDir, "conda-meta").isDirectory()) { + if (isCondaDir) { // This environment has already been populated. // TODO: Should we update it? For now, we just use it. fillConfig(conda, envDir, builder.config); diff --git a/src/test/java/org/apposed/appose/ApposeTest.java b/src/test/java/org/apposed/appose/ApposeTest.java index 180295d..599d73d 100644 --- a/src/test/java/org/apposed/appose/ApposeTest.java +++ b/src/test/java/org/apposed/appose/ApposeTest.java @@ -119,7 +119,9 @@ public void testConda() throws IOException, InterruptedException { @Test public void testServiceStartupFailure() throws IOException, InterruptedException { - Environment env = Appose.build("no-pythons-to-be-found-here"); + String tempNonExistingDir = "no-pythons-to-be-found-here"; + new File(tempNonExistingDir).deleteOnExit(); + Environment env = Appose.build(tempNonExistingDir); try (Service service = env.python()) { String info = ""; try { From 18e09b938dc457e62ed8648b47e6e96cd8f294d9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 23 Sep 2024 14:37:36 -0500 Subject: [PATCH 118/120] Make env building debug output nicer Yellow for stdout, red for stderr. --- src/main/java/org/apposed/appose/Builder.java | 11 ++++++----- src/main/java/org/apposed/appose/mamba/Mamba.java | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apposed/appose/Builder.java b/src/main/java/org/apposed/appose/Builder.java index 6b4f2ce..8b13496 100644 --- a/src/main/java/org/apposed/appose/Builder.java +++ b/src/main/java/org/apposed/appose/Builder.java @@ -99,11 +99,12 @@ public Builder subscribeError(Consumer subscriber) { * @return This {@code Builder} instance, for fluent-style programming. */ public Builder logDebug() { - return subscribeProgress( - (title, cur, max) -> System.out.printf("%s: %d/%d\n", title, cur, max) - ) - .subscribeOutput(msg -> System.out.printf("[stdout] %s\n", msg)) - .subscribeError(msg -> System.out.printf("[stderr] %s\n", msg)); + String reset = "\u001b[0m"; + String yellow = "\u001b[0;33m"; + String red = "\u001b[0;31m"; + return subscribeProgress((title, cur, max) -> System.out.printf("%s: %d/%d\n", title, cur, max)) + .subscribeOutput(msg -> System.out.printf("%s%s%s", yellow, msg.isEmpty() ? "." : msg, reset)) + .subscribeError(msg -> System.out.printf("%s%s%s", red, msg.isEmpty() ? "." : msg, reset)); } /** diff --git a/src/main/java/org/apposed/appose/mamba/Mamba.java b/src/main/java/org/apposed/appose/mamba/Mamba.java index 9cb2cf3..f671731 100644 --- a/src/main/java/org/apposed/appose/mamba/Mamba.java +++ b/src/main/java/org/apposed/appose/mamba/Mamba.java @@ -542,7 +542,7 @@ public void runMamba(boolean isInheritIO, final String... args ) throws RuntimeE } updateErrorConsumer(errChunk); updateOutputConsumer(processChunk + System.lineSeparator() - + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED PROCESS"); + + sdf.format(Calendar.getInstance().getTime()) + " -- TERMINATED PROCESS\n"); } catch (IOException | InterruptedException e) { e.printStackTrace(); } From 59461a8b7d291e967134a446240a07b4825b8c51 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 23 Sep 2024 16:26:07 -0500 Subject: [PATCH 119/120] Resolve documentation TODOs in Appose.java --- src/main/java/org/apposed/appose/Appose.java | 101 +++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apposed/appose/Appose.java b/src/main/java/org/apposed/appose/Appose.java index a84d5f6..53f75e4 100644 --- a/src/main/java/org/apposed/appose/Appose.java +++ b/src/main/java/org/apposed/appose/Appose.java @@ -50,10 +50,6 @@ * {@link Service.Task#listen via callbacks}. * *

Examples

- *
    - *
  • TODO - move the below code somewhere linkable, for succinctness - * here.
  • - *
*

* Here is a very simple example written in Java: *

@@ -144,11 +140,104 @@ *
  • The worker must issue responses in Appose's response format on its * standard output (stdout) stream.
  • * + *

    Requests to worker from service

    *

    - * TODO - write up the request and response formats in detail here! - * JSON, one line per request/response. + * A request is a single line of JSON sent to the worker process via + * its standard input stream. It has a {@code task} key taking the form of a + * UUID, + * and a {@code requestType} key with one of the following values: *

    + *

    EXECUTE

    + *

    + * Asynchronously execute a script within the worker process. E.g.: + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "requestType" : "EXECUTE",
    + *    "script" : "task.outputs[\"result\"] = computeResult(gamma)\n",
    + *    "inputs" : {"gamma": 2.2}
    + * }
    + * 
    + *

    CANCEL

    + *

    + * Cancel a running script. E.g.: + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "requestType" : "CANCEL"
    + * }
    + * 
    * + *

    Responses from worker to service

    + *

    + * A response is a single line of JSON with a {@code task} key + * taking the form of a + * UUID, + * and a {@code responseType} key with one of the following values: + *

    + *

    LAUNCH

    + *

    + * A LAUNCH response is issued to confirm the success of an EXECUTE request. + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "responseType" : "LAUNCH"
    + * }
    + * 
    + *

    UPDATE

    + *

    + * An UPDATE response is issued to convey that a task has somehow made + * progress. The UPDATE response typically comes bundled with a {@code message} + * string indicating what has changed, {@code current} and/or {@code maximum} + * progress indicators conveying the step the task has reached, or both. + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "responseType" : "UPDATE",
    + *    "message" : "Processing step 0 of 91",
    + *    "current" : 0,
    + *    "maximum" : 91
    + * }
    + * 
    + *

    COMPLETION

    + *

    + * A COMPLETION response is issued to convey that a task has successfully + * completed execution, as well as report the values of any task outputs. + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "responseType" : "COMPLETION",
    + *    "outputs" : {"result" : 91}
    + * }
    + * 
    + *

    CANCELATION

    + *

    + * A CANCELATION response is issued to confirm the success of a CANCEL request. + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "responseType" : "CANCELATION"
    + * }
    + * 
    + *

    FAILURE

    + *

    + * A FAILURE response is issued to convey that a task did not completely + * and successfully execute, such as an exception being raised. + *

    + *
    
    + * {
    + *    "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
    + *    "responseType" : "FAILURE",
    + *    "error", "Invalid gamma value"
    + * }
    + * 
    + * * @author Curtis Rueden */ public class Appose { From 4ea317bbd675533af281ac9339ffe242167adc98 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 24 Sep 2024 11:44:34 -0500 Subject: [PATCH 120/120] Fix shm test failures on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, the size of the allocated shared memory block might not precisely match the request. But it needs to be at least as large as the requested size. E.g.: sizes less than 16384 round up to 16384. You might think it would be a good idea to make the size() method of SharedMemory return the actually requested size, rather than the weird rounded up size, but that's how it works on macOS on Python, and we're trying to make the Java implementation match the Python behavior as closely as possible, so... ¯\_(ツ)_/¯ --- .../java/org/apposed/appose/SharedMemoryTest.java | 9 +++++---- src/test/java/org/apposed/appose/TypesTest.java | 15 +++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/apposed/appose/SharedMemoryTest.java b/src/test/java/org/apposed/appose/SharedMemoryTest.java index 59b4e6a..05fd7c2 100644 --- a/src/test/java/org/apposed/appose/SharedMemoryTest.java +++ b/src/test/java/org/apposed/appose/SharedMemoryTest.java @@ -41,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests {@link SharedMemory}. @@ -54,7 +55,7 @@ public void testShmCreate() throws IOException { int size = 456; try (SharedMemory shm = SharedMemory.create(null, size)) { assertNotNull(shm.name()); - assertEquals(size, shm.size()); + assertTrue(shm.size() >= size); assertNotNull(shm.pointer()); // Modify the memory contents. @@ -74,8 +75,8 @@ public void testShmCreate() throws IOException { String output = runPython( "from multiprocessing.shared_memory import SharedMemory\n" + "from sys import stdout\n" + - "shm = SharedMemory(name='" + shm.name() + "', size=" + shm.size() + ")\n" + - "matches = sum(1 for i in range(shm.size) if shm.buf[i] == (shm.size - i) % 256)\n" + + "shm = SharedMemory(name='" + shm.name() + "', size=" + size + ")\n" + + "matches = sum(1 for i in range(" + size + ") if shm.buf[i] == (" + size + " - i) % 256)\n" + "stdout.write(f'{matches}\\n')\n" + "stdout.flush()\n" + "shm.unlink()\n" // HACK: to satisfy Python's overly aggressive resource tracker @@ -112,7 +113,7 @@ public void testShmAttach() throws IOException { assertNotNull(shmName); assertFalse(shmName.isEmpty()); int shmSize = Integer.parseInt(shmInfo[1]); - assertEquals(345, shmSize); + assertTrue(shmSize >= 345); // Attach to the shared memory and verify it matches expectations. try (SharedMemory shm = SharedMemory.attach(shmName, shmSize)) { diff --git a/src/test/java/org/apposed/appose/TypesTest.java b/src/test/java/org/apposed/appose/TypesTest.java index 06e7e39..2d218fe 100644 --- a/src/test/java/org/apposed/appose/TypesTest.java +++ b/src/test/java/org/apposed/appose/TypesTest.java @@ -70,7 +70,7 @@ public class TypesTest { "\"shm\":{" + "\"appose_type\":\"shm\"," + "\"name\":\"SHM_NAME\"," + - "\"size\":4000" + + "\"size\":SHM_SIZE" + "}" + "}" + "}"; @@ -110,7 +110,9 @@ public void testEncode() { data.put("ndArray", ndArray); String json = Types.encode(data); assertNotNull(json); - String expected = JSON.replaceAll("SHM_NAME", ndArray.shm().name()); + String expected = JSON + .replaceAll("SHM_NAME", ndArray.shm().name()) + .replaceAll("SHM_SIZE", "" + ndArray.shm().size()); assertEquals(expected, json); } } @@ -119,11 +121,16 @@ public void testEncode() { public void testDecode() { Map data; String shmName; + int shmSize; // Create name shared memory segment and decode JSON block. try (SharedMemory shm = SharedMemory.create(null, 4000)) { shmName = shm.name(); - data = Types.decode(JSON.replaceAll("SHM_NAME", shmName)); + shmSize = shm.size(); + String json = JSON + .replaceAll("SHM_NAME", shmName) + .replaceAll("SHM_SIZE", "" + shmSize); + data = Types.decode(json); } // Validate results. @@ -158,7 +165,7 @@ public void testDecode() { assertEquals(20, ndArray.shape().get(1)); assertEquals(25, ndArray.shape().get(2)); assertEquals(shmName, ndArray.shm().name()); - assertEquals(4000, ndArray.shm().size()); + assertEquals(shmSize, ndArray.shm().size()); } }