diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 384e7fd..d860a86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,11 @@ jobs: cache: 'maven' - name: Build with Maven - run: mvn package + run: mvn package -DskipTests=true - - name: Runnig the example + - name: Running tests + run: mvn test + + - name: Running the example run: java -cp "./rust-maven-example/target/rust-maven-example-1.0.0-SNAPSHOT.jar${{ matrix.path-sep }}./jar-jni/target/jar-jni-1.0.0-SNAPSHOT.jar" io.questdb.example.rust.Main + diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 3f73112..716d539 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,8 +7,8 @@ + - diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e012065 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/README.md b/README.md index 61bc76c..27f11c0 100644 --- a/README.md +++ b/README.md @@ -2,53 +2,326 @@ rust-maven-plugin -Build Rust Cargo crates within a Java Maven Project with this plugin. +Build Rust Cargo crates within a Java Maven Project. -The use case is to simplify the build process of -[Rust JNI libs](https://crates.io/crates/jni) inside a Java -[Maven](https://maven.apache.org/) project. - -# Features - -The plugin provides the following features: +```shell +$ mvn clean package +... +[INFO] --- rust-maven-plugin:1.0.0-SNAPSHOT:build (str-reverse) @ rust-maven-example --- +[INFO] Working directory: /home/adam/questdb/repos/rust-maven-plugin/rust-maven-example/src/main/rust/str-reverse +[INFO] Environment variables: +[INFO] REVERSED_STR_PREFIX='Great Scott, A reversed string!' +[INFO] Running: cargo build --target-dir /home/adam/questdb/repos/rust-maven-plugin/rust-maven-example/target/rust-maven-plugin/str-reverse --release +[INFO] Compiling proc-macro2 v1.0.49 +[INFO] Compiling quote v1.0.23 +[INFO] Compiling unicode-ident v1.0.6 +[INFO] Compiling syn v1.0.107 +... +``` -* Calls `cargo build` as part of a `mvn compile` step. +# Plugin Features -* If Rust isn't found, points users to https://www.rust-lang.org/tools/install. +* The plugin delegates the build to `cargo` and supports most of `cargo build`'s features. +* The primary use case is to simplify the build process of + [Rust JNI libs](https://crates.io/crates/jni) inside a Java + [Maven](https://maven.apache.org/) project. +* Additionally, the plugin can also compile binaries. +* The plugin can copy complied binaries to a custom location and so they can be bundled inside of `.jar` files. +* Support for invoking `cargo test` during `mvn test`. +* Points to https://www.rust-lang.org/tools/install if `cargo` isn't found. -* Builds inside Maven's target directory: - * Passes `${project.build.directory}/rust-maven-plugin/${crate_name}` as - `cargo build --target-dir`. - * Thus, `mvn clean` also cleans the Rust crates without additional `pom.xml` - setup. +## Optional supporting loader library -* Optionally: - * Can be configured to copy the compiled cdylib(s) to the - `${project.build.directory}/classes/` directory so the binaries ends up - bundled in the JAR. +For your convenience, we've also made `jar-jni` available: +An optional Java library to load JNI dynamic libraries from JARs. - * As a separate runtime dependency we also provide [`jar-jni`](jar-jni/), - a helper java library to load the libs from the JAR using a sub-directory - per `os.name`/`os.arch` so the JAR can contain binaries for multiple - platforms. +Both the plugin and the library support a directory naming convention structure to support compiling +for a multitude of platforms. -# Example +# Complete Example See the [`rust-maven-example`](rust-maven-example/) directory for a working example. -# Status -* Pre-production: - * Building, cleaning, and bundling into a `.jar` now works. - * Running Rust tests (calling `cargo test`) isn't implemented yet. - * Not yet available on maven central: - In the meantime `cd rust-maven-plugin && mvn install`. +It also uses the `jar-jni` library to load the Rust binaries from the compiled +JAR file. + +# Basic Configuration + +Edit your `pom.xml` to add the plugin: + +```xml + + ... + + + + + + + io.questdb + rust-maven-plugin + 1.0.0-SNAPSHOT + + + rust-build-id + + build + + + src/main/rust/your-rust-crate + ${project.build.directory}/classes/io/questdb/example/rust/libs + true + + + + + ... + + + + +``` + +Here, `..` is the path to the Rust crate to build, and it is relative to the `pom.xml` file itself. +The plugin will invoke `cargo build` on the crate. + +The `` is an arbitrary string that you can use to identify the execution. +It does not need to match the crate name. + +If you need to build multiple crates, you can add multiple executions. + +# Testing + +The plugin can also invoke `cargo test` during `mvn test`. + +To enable running tests: + +* Duplicate the `` block above. +* Change it's `` to a new name. +* Change the `` to `test`. + +```xml + + str-reverse-test + + test + + + src/main/rust/str-reverse + + +``` + +# Customizing the build and tests steps + +The settings below go in the `` section of the `` block. + +## Custom path to the `cargo` command + +If `cargo` isn't in your `PATH`, you can specify the path to the `cargo` command with the `` +configuration option. + +You can also specify this on the command line via `mvn ... -DcargoPath=...`. + +## Verbosity + +The plugin can be configured to forward various verbosity flags to `cargo` by setting +`-v` (or other value) in the `` block. + +Accepted values are: + +| Value | Description | +|---------------------------------------|-------------------------------------------------| +| `` (or no tag) | Default - no additional flags passed to `cargo` | +| `-q` | Quiet | +| `-v` | Verbose | +| `-vv` | Very verbose | + +## Release builds + +The plugin can be configured to build in release mode by setting +`true` in the `` block. + +## Specifying Crate Features + +The equivalent of `cargo build --features feat1,feat2,feat3` is + +```xml + + feat1 + feat2 + feat3 + +``` + +You can also specify `--all-features` via `true` and `--no-default-features` +via `true`. + +## Additional cargo arguments + +Additional arguments to can go in the `` configuration section. + +```xml + + --verbose + --color=always + +``` + +## Overriding Environment Variables + +The plugin can be configured to override environment variables during the build. +This might be useful for setting `RUSTFLAGS`. + +In the `` section, add: -If you want to help out, check out our -[open issues](https://github.com/questdb/rust-maven-plugin/issues). +```xml + + -C target-cpu=native + +``` + +# Cleaning the Rust build + +Regular `mvn clean` will also clean the Rust build without additional config. +This is because the plugin builds crates inside Maven's `target` build +directory, via `cargo build --target-dir ...`. + +# De-duplicating build directories when invoking `cargo build` without Maven + +If you (or your IDE) end up invoking `cargo build` on your Rust crate without +the plugin, you'll notice this creates a duplicate `target` dir that will not +be cleaned at the next `mvn clean`. + +To avoid this duplicate `target` directory problem, consider adding +`.cargo/config.toml` files configured to match the `--target-dir` argument +passed by this plugin. + +See [.cargo/config.toml](rust-maven-example/src/main/rust/str-reverse/.cargo/config.toml) +from the `str-reverse` crate in the example. + +# Bundling binaries in the `.jar` file + +The `` configuration allows copying the binaries anywhere. The example +however choses to copy them to `${project.build.directory}/classes/...`. +Anything placed there gets bundled in the JAR file. +The `classes` directory sits within the `target` directory and outside of the +source tree. + +## Binaries in source tree + +Placing binaries in the source tree may be the "pragmatic" approach if you need +to support IntelliJ which, by default, will not actually invoke `maven compile` +during its usual operation. -# Dev Commands +If you know a better way around this in IntelliJ do contact us! -To build the project and example: +If you prefer to keep your binaries in the source tree, then you instead +configure to copy binaries to the [`resources`](https://stackoverflow.com/questions/25786185/what-is-the-purpose-for-the-resource-folder-in-maven) directory +instead: + +```xml +src/main/resources/io/questdb/example/rust/libs +true +``` + +In such case, you may opt to move the `rust-maven-plugin` inside a +[Maven Profile](https://maven.apache.org/guides/introduction/introduction-to-profiles.html) and only build the Rust +code when you need to. + +```xml + + ... + + + rust + + + + io.questdb + rust-maven-plugin + 1.0.0-SNAPSHOT +... +``` + +You can then enable the profile in Maven via `mvn clean package -Prust ...`. + +## Supporting Multiple Platforms + +During the binary copy step, the `true` config setting (used in the examples +above) will further nest the binaries in a directory named after the platform. + +``` +target + classes + io/questdb/example/rust/libs/ + linux-amd64/libstr_reverse.so + mac_os_x-aarch64/libstr_reverse.dylib + windows-amd64/str_reverse.dll +``` + +If you only intend to target one single platform (e.g. linux-amd64), then you +don't need `true` and the plugin will not create a nested directory. + +# Loading binaries from the `.jar` with `jar-jni` + +The `jar-jni` library is configured as so: + +```xml + + + ... + + + io.questdb + jar-jni + 1.0.0-SNAPSHOT + + + ... + +``` + +It helps with bundling JNI native code in `.jar` files by establishing a directory +naming convention for organising binaries for different operating systems and +architectures. + +Assuming you've compiled with `true`, load +the binary from the `.jar` file with: + +```java +JarJniLoader.loadLib( + Main.class, + + // A platform-specific path is automatically suffixed to path below. + "/io/questdb/example/rust/libs", + + // The "lib" prefix and ".so|.dynlib|.dll" suffix are added automatically as needed. + "str_reverse"); +``` + +If instead you compiled with `false`, then: + +```java +JarJniLoader.loadLib( + Main.class, + "/io/questdb/example/rust/libs", + "str_reverse", + null); +``` + +# Contributing & Support + +* Test cases, features, docs, tutorials, etc are always welcome. +* [Raise an issue](https://github.com/questdb/rust-maven-plugin/issues/new/choose) if you find bugs. +* We've got a list of open [issues](https://github.com/questdb/rust-maven-plugin/issues). +* Raise a pull request if you need a new feature. + +If you want to talk to us, we're on [Slack](https://slack.questdb.io/). + +## Building + +To build the project and run the example: ```shell git clone https://github.com/questdb/rust-maven-plugin.git @@ -57,15 +330,18 @@ mvn clean package java -cp "./rust-maven-example/target/rust-maven-example-1.0.0-SNAPSHOT.jar:./jar-jni/target/jar-jni-1.0.0-SNAPSHOT.jar" io.questdb.example.rust.Main ``` -To run Maven goals directly from the command line. +## Testing Against Another Project + +For test your changes against another project you need to install the `jar-jni` and `rust-maven-plugin` artifacts locally in your Maven cache: ```shell -cd rust-maven-plugin -mvn install -mvn io.questdb:rust-maven-plugin:build -Drelease=true +cd jar-jni +mnv clean install +cd ../rust-maven-plugin +mvn clean install ``` -# Special thanks +## Thanks to -* OktaDev for covering custom Maven plugins on YouTube: https://www.youtube.com/watch?v=wHX4j0z-sUU -* The CMake maven plugin project: https://github.com/cmake-maven-project/cmake-maven-project +* OktaDev for covering custom Maven plugins on YouTube https://www.youtube.com/watch?v=wHX4j0z-sUU - It's a great introduction to Maven plugins. +* The CMake maven plugin project https://github.com/cmake-maven-project/cmake-maven-project for inspiration. diff --git a/artwork/logo_outline_text.svg b/artwork/logo_outline_text.svg index 1df3b14..a5f5240 100644 --- a/artwork/logo_outline_text.svg +++ b/artwork/logo_outline_text.svg @@ -8,7 +8,7 @@ version="1.1" id="svg5" sodipodi:docname="logo_outline_text.svg" - inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + inkscape:version="1.2.2 (732a01da63, 2022-12-09, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" @@ -25,18 +25,20 @@ inkscape:document-units="mm" showgrid="false" inkscape:zoom="5.8379116" - inkscape:cx="112.62589" - inkscape:cy="129.58401" + inkscape:cx="112.54024" + inkscape:cy="129.7553" inkscape:window-width="3840" - inkscape:window-height="2091" - inkscape:window-x="0" - inkscape:window-y="32" + inkscape:window-height="2096" + inkscape:window-x="2560" + inkscape:window-y="27" inkscape:window-maximized="1" inkscape:current-layer="layer6" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" - fit-margin-bottom="0" /> + fit-margin-bottom="0" + inkscape:showpageshadow="2" + inkscape:deskcolor="#d1d1d1" /> + gradientTransform="translate(1.1900047,-1.1466698)" /> - rustmavenplugin + + + + + + + + + + + + + + + + + $$VERSION$$"], + 'README.md': + ["rust-maven-plugin:$$VERSION$$:build", + "$$VERSION$$", + "-$$VERSION$$.jar"], + 'jar-jni/pom.xml': + ["$$VERSION$$"], + 'rust-maven-plugin/pom.xml': + ["$$VERSION$$"], + 'rust-maven-example/pom.xml': + ["$$VERSION$$"], +} + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('version', help='Version number to change to') + parser.add_argument('-p', '--preview', action='store_true', + help='Preview changes without making them') + parser.add_argument('-n', '--num-lines', type=int, default=3, + help='Number of lines of context to show in diff') + parser.add_argument('--no-color', action='store_true', + help='Disable color-coded diff output') + return parser.parse_args() + + +def main(): + args = parse_args() + old_contents = {} + new_contents = {} + for filename, matchers in MATCHERS.items(): + file_path = pathlib.Path(filename) + with open(file_path, 'r', encoding='utf-8') as f: + contents = f.read() + old_contents[file_path] = contents + + for matcher in matchers: + old_version_text = matcher.replace('$$VERSION$$', CURRENT_VERSION) + new_version_text = matcher.replace('$$VERSION$$', args.version) + if old_version_text not in contents: + raise RuntimeError( + 'Could not find "{}" in file "{}"'.format( + old_version_text, file_path)) + contents = contents.replace(old_version_text, new_version_text) + + new_contents[file_path] = contents + + for file_path, new_text in new_contents.items(): + if args.preview: + old_text = old_contents[file_path] + filename = str(file_path) + colors = not args.no_color + if colors: + green = '\x1b[38;5;16;48;5;2m' + red = '\x1b[38;5;16;48;5;1m' + end = '\x1b[0m' + else: + green = '' + red = '' + end = '' + diff = difflib.unified_diff( + old_text.splitlines(keepends=True), + new_text.splitlines(keepends=True), + fromfile=filename, + tofile=filename, + lineterm='', + n=args.num_lines) + if colors: + for line in diff: + if line.startswith('+') and not line.startswith('+++'): + print(green + line + end, end='') + elif line.startswith('-') and not line.startswith('---'): + print(red + line + end, end='') + else: + print(line, end='') + else: + for line in diff: + print(line, end='') + else: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_text) + + +if __name__ == '__main__': + main() + diff --git a/jar-jni/pom.xml b/jar-jni/pom.xml index 0577f81..642c501 100644 --- a/jar-jni/pom.xml +++ b/jar-jni/pom.xml @@ -33,11 +33,124 @@ JAR JNI Loader Load JNI dependencies embedded within a JAR file. + https://github.com/questdb/rust-maven-plugin + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + QuestDB Team + hello@questdb.io + QuestDB + https://questdb.io/ + + + + https://github.com/questdb/rust-maven-plugin + scm:git:https://github.com/questdb/rust-maven-plugin.git + scm:git:git@github.com:questdb/rust-maven-plugin.git + HEAD + + + + central + https://oss.sonatype.org/content/repositories/snapshots + + + central + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + 1.8 1.8 UTF-8 UTF-8 + ${project.basedir}/src/main/java9 + ${project.build.directory}/classes-java9 + + + + java-module + + [9,) + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + compile-java9 + compile + + run + + + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.0 + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/META-INF/versions/9 + + + ${java9.build.outputDirectory} + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + + + + + + + + \ No newline at end of file diff --git a/jar-jni/src/main/java/io/questdb/jar/jni/JarJniLoader.java b/jar-jni/src/main/java/io/questdb/jar/jni/JarJniLoader.java index b3e739a..515fb70 100644 --- a/jar-jni/src/main/java/io/questdb/jar/jni/JarJniLoader.java +++ b/jar-jni/src/main/java/io/questdb/jar/jni/JarJniLoader.java @@ -26,14 +26,29 @@ import java.io.File; import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; +/** + * Loads native libraries from JAR files. + */ public interface JarJniLoader { - - static void loadLib(Class cls, String jarPathPrefix, LibInfo lib) { + /** + * Loads a native library from a JAR file. + * + * @param cls The class to use for loading the library. + * @param jarPathPrefix The path prefix to the library in the JAR file. + * @param name The name of the library, sans "lib" prefix and ".so|.dll|.dylib" suffix. + * @param platformDir The platform-specific subdirectory (inside jarPathPrefix) to load the library from, + * or null to search for the dynamic library directly within jarPathPrefix. + */ + static void loadLib(Class cls, String jarPathPrefix, String name, String platformDir) { final String sep = jarPathPrefix.endsWith("/") ? "" : "/"; - final String pathInJar = jarPathPrefix + sep + lib.getPath(); + String pathInJar = jarPathPrefix + sep; + if (platformDir != null) { + pathInJar += platformDir + "/"; + } + pathInJar += OsInfo.LIB_PREFIX + name + OsInfo.LIB_SUFFIX; final InputStream is = cls.getResourceAsStream(pathInJar); if (is == null) { throw new LoadException("Internal error: cannot find " + pathInJar + ", broken package?"); @@ -46,14 +61,7 @@ static void loadLib(Class cls, String jarPathPrefix, LibInfo lib) { tempLib = File.createTempFile(pathInJar.substring(0, dot), pathInJar.substring(dot)); // copy to tempLib try (FileOutputStream out = new FileOutputStream(tempLib)) { - byte[] buf = new byte[4096]; - while (true) { - int read = is.read(buf); - if (read == -1) { - break; - } - out.write(buf, 0, read); - } + StreamTransfer.copyToStream(is, out); } finally { tempLib.deleteOnExit(); } @@ -70,7 +78,26 @@ static void loadLib(Class cls, String jarPathPrefix, LibInfo lib) { } } - static void loadLib(Class cls, String jarPathPrefix, String libName) { - loadLib(cls, jarPathPrefix, new LibInfo(libName)); + /** + * Loads a native library from a JAR file. + *

+ * The library is loaded from the platform-specific subdirectory of the jarPathPrefix. + *

+ * The platform-specific subdirectory derived from the current platform and + * the architecture of the JVM as determined by {@link OsInfo#PLATFORM}. + *

+ * For example if executing, JarJniLoader.loadLib(MyClass.class, "/native", "mylib"); + * on a 64-bit x86 Linux system, the library will be loaded from "/native/linux-amd64/libmylib.so" + * from the same JAR file that contains MyClass.class. + * If executing on an Apple Silicon Macbook, the library will be loaded from + * "/native/mac_os_x-arm64/libmylib.dylib". + * From Windows 11, the library will be loaded from "/native/windows-amd64/mylib.dll" (note, no "lib" prefix). + * + * @param cls The class to use for loading the library. + * @param jarPathPrefix The path prefix to the library in the JAR file. + * @param name The name of the library, sans "lib" prefix and ".so|.dll|.dylib" suffix. + */ + static void loadLib(Class cls, String jarPathPrefix, String name) { + loadLib(cls, jarPathPrefix, name, OsInfo.PLATFORM); } } diff --git a/jar-jni/src/main/java/io/questdb/jar/jni/LibInfo.java b/jar-jni/src/main/java/io/questdb/jar/jni/LibInfo.java deleted file mode 100644 index 1dfd1fb..0000000 --- a/jar-jni/src/main/java/io/questdb/jar/jni/LibInfo.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.questdb.jar.jni; - -public class LibInfo { - private String platform; - private String name; - private String prefix; - private String suffix; - - public LibInfo(String name) { - this( - OsInfo.INSTANCE.getPlatform(), - name, - OsInfo.INSTANCE.getLibPrefix(), - OsInfo.INSTANCE.getLibSuffix() - ); - } - - public LibInfo(String platform, String name, String prefix, String suffix) { - this.platform = platform; - this.name = name; - this.prefix = prefix; - this.suffix = suffix; - } - - public String getPlatform() { - return platform; - } - - public String getName() { - return name; - } - - public String getPrefix() { - return prefix; - } - - public String getSuffix() { - return suffix; - } - - public String getFullName() { - return prefix + name + suffix; - } - - public String getPath() { - return platform + "/" + getFullName(); - } -} diff --git a/jar-jni/src/main/java/io/questdb/jar/jni/LoadException.java b/jar-jni/src/main/java/io/questdb/jar/jni/LoadException.java index 57adee5..0cc555a 100644 --- a/jar-jni/src/main/java/io/questdb/jar/jni/LoadException.java +++ b/jar-jni/src/main/java/io/questdb/jar/jni/LoadException.java @@ -1,5 +1,32 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + package io.questdb.jar.jni; +/** + * Exception thrown when a native library cannot be loaded. + */ public class LoadException extends RuntimeException { public LoadException(String message) { super(message); diff --git a/jar-jni/src/main/java/io/questdb/jar/jni/OsInfo.java b/jar-jni/src/main/java/io/questdb/jar/jni/OsInfo.java index e70043d..83266c7 100644 --- a/jar-jni/src/main/java/io/questdb/jar/jni/OsInfo.java +++ b/jar-jni/src/main/java/io/questdb/jar/jni/OsInfo.java @@ -1,29 +1,70 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + package io.questdb.jar.jni; -public enum OsInfo { - INSTANCE(); +/** + * Information about the current operating system. + */ +public abstract class OsInfo { + + /** + * The platform name, e.g. "linux-aarch64", "mac_os_x-x86_64" or "windows-amd64" + */ + public static final String PLATFORM; + + /** + * The prefix for native libraries, e.g. "lib", or "" on Windows. + */ + public static final String LIB_PREFIX; + + /** + * The suffix for native libraries, e.g. ".so", ".dylib" or ".dll". + */ + public static final String LIB_SUFFIX; - private final String platform; - private final String libPrefix; - private final String libSuffix; + /** + * The suffix for executables, e.g. ".exe" or "" on Unix. + */ + public static final String EXE_SUFFIX; - OsInfo() { - final String osName = System.getProperty("os.name").toLowerCase(); + static { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + osName = "windows"; // Too many flavours, binaries are compatible. + } final String osArch = System.getProperty("os.arch").toLowerCase(); - this.platform = (osName + "-" + osArch).replace(' ', '_'); - this.libPrefix = osName.startsWith("windows") ? "" : "lib"; - this.libSuffix = osName.startsWith("windows") - ? ".dll" : osName.contains("mac") - ? ".dylib" : ".so"; - } + PLATFORM = (osName + "-" + osArch).replace(' ', '_'); - public String getPlatform() { return platform; } + LIB_PREFIX = osName.startsWith("windows") ? "" : "lib"; - public String getLibPrefix() { - return libPrefix; - } + LIB_SUFFIX = osName.startsWith("windows") ? ".dll" + : osName.contains("mac") ? ".dylib" + : ".so"; - public String getLibSuffix() { - return libSuffix; + EXE_SUFFIX = osName.startsWith("windows") + ? ".exe" : ""; } -} \ No newline at end of file +} + diff --git a/jar-jni/src/main/java/io/questdb/jar/jni/StreamTransfer.java b/jar-jni/src/main/java/io/questdb/jar/jni/StreamTransfer.java new file mode 100644 index 0000000..966e651 --- /dev/null +++ b/jar-jni/src/main/java/io/questdb/jar/jni/StreamTransfer.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.jar.jni; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface StreamTransfer { + static void copyToStream(InputStream is, OutputStream out) throws IOException { + byte[] buf = new byte[4096]; + while (true) { + int read = is.read(buf); + if (read == -1) { + break; + } + out.write(buf, 0, read); + } + } +} diff --git a/jar-jni/src/main/java9/io/questdb/jar/jni/StreamTransfer.java b/jar-jni/src/main/java9/io/questdb/jar/jni/StreamTransfer.java new file mode 100644 index 0000000..91a49c1 --- /dev/null +++ b/jar-jni/src/main/java9/io/questdb/jar/jni/StreamTransfer.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.jar.jni; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface StreamTransfer { + static void copyToStream(InputStream is, OutputStream out) throws IOException { + is.transferTo(out); + } +} diff --git a/jar-jni/src/main/java9/module-info.java b/jar-jni/src/main/java9/module-info.java new file mode 100644 index 0000000..40139b7 --- /dev/null +++ b/jar-jni/src/main/java9/module-info.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +open module io.questdb.jar.jni { + requires java.base; + exports io.questdb.jar.jni; +} diff --git a/rust-maven-example/pom.xml b/rust-maven-example/pom.xml index b694a3c..a2a26e9 100644 --- a/rust-maven-example/pom.xml +++ b/rust-maven-example/pom.xml @@ -107,10 +107,8 @@ ${project.build.directory}/classes/io/questdb/example/rust/libs true @@ -123,6 +121,7 @@ + str-reverse-binary build @@ -131,6 +130,39 @@ src/main/rust/str-reverse-binary true ${project.build.directory}/bin + + header + footer + + + + + str-reverse-test + + + + test + + + + + src/main/rust/str-reverse + + + false + + -v + + + Testing prefix + diff --git a/rust-maven-example/src/main/java/io/questdb/example/rust/Main.java b/rust-maven-example/src/main/java/io/questdb/example/rust/Main.java index 31791fa..3f1df27 100644 --- a/rust-maven-example/src/main/java/io/questdb/example/rust/Main.java +++ b/rust-maven-example/src/main/java/io/questdb/example/rust/Main.java @@ -27,8 +27,6 @@ import io.questdb.jar.jni.JarJniLoader; public class Main { - public static native String reversedString(String str); - static { JarJniLoader.loadLib( Main.class, @@ -40,6 +38,8 @@ public class Main { "str_reverse"); } + public static native String reversedString(String str); + public static void main(String[] args) { System.out.println(reversedString("Hello World!")); } diff --git a/rust-maven-example/src/main/rust/str-reverse-binary/.cargo/config.toml b/rust-maven-example/src/main/rust/str-reverse-binary/.cargo/config.toml new file mode 100644 index 0000000..f09c03e --- /dev/null +++ b/rust-maven-example/src/main/rust/str-reverse-binary/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +# /target/rust-maven-plugin/str-reverse-binary +target-dir = "../../../../target/rust-maven-plugin/str-reverse-binary" diff --git a/rust-maven-example/src/main/rust/str-reverse-binary/Cargo.toml b/rust-maven-example/src/main/rust/str-reverse-binary/Cargo.toml index a8b7b32..329a2b8 100644 --- a/rust-maven-example/src/main/rust/str-reverse-binary/Cargo.toml +++ b/rust-maven-example/src/main/rust/str-reverse-binary/Cargo.toml @@ -1,4 +1,8 @@ [package] name = "str-reverse-binary" version = "0.1.0" -edition = "2021" \ No newline at end of file +edition = "2021" + +[features] +header = [] +footer = [] diff --git a/rust-maven-example/src/main/rust/str-reverse-binary/src/main.rs b/rust-maven-example/src/main/rust/str-reverse-binary/src/main.rs index eb9daf4..70595ad 100644 --- a/rust-maven-example/src/main/rust/str-reverse-binary/src/main.rs +++ b/rust-maven-example/src/main/rust/str-reverse-binary/src/main.rs @@ -31,7 +31,15 @@ fn main() -> ExitCode { match args.len() { 2 => { let reversed: String = args[1].chars().rev().collect(); + + #[cfg(feature = "header")] + println!(">>>>>>>>>>>>>>>>>>>>>>>>>"); + println!("{}", reversed); + + #[cfg(feature = "footer")] + println!("<<<<<<<<<<<<<<<<<<<<<<<<<<"); + ExitCode::SUCCESS }, _ => { diff --git a/rust-maven-example/src/main/rust/str-reverse/.cargo/config.toml b/rust-maven-example/src/main/rust/str-reverse/.cargo/config.toml new file mode 100644 index 0000000..2ec46cd --- /dev/null +++ b/rust-maven-example/src/main/rust/str-reverse/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +# /target/rust-maven-plugin/str-reverse +target-dir = "../../../../target/rust-maven-plugin/str-reverse" diff --git a/rust-maven-example/src/main/rust/str-reverse/Cargo.toml b/rust-maven-example/src/main/rust/str-reverse/Cargo.toml index 3b7b0e1..1642a38 100644 --- a/rust-maven-example/src/main/rust/str-reverse/Cargo.toml +++ b/rust-maven-example/src/main/rust/str-reverse/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -crate_type = ["cdylib"] +crate-type = ["cdylib"] [dependencies] jni = "0.20.0" \ No newline at end of file diff --git a/rust-maven-example/src/main/rust/str-reverse/src/lib.rs b/rust-maven-example/src/main/rust/str-reverse/src/lib.rs index 9a39217..5c37804 100644 --- a/rust-maven-example/src/main/rust/str-reverse/src/lib.rs +++ b/rust-maven-example/src/main/rust/str-reverse/src/lib.rs @@ -39,3 +39,11 @@ pub extern "system" fn Java_io_questdb_example_rust_Main_reversedString( .expect("Couldn't create java string!"); output.into_raw() } + +#[cfg(test)] +mod tests { + #[test] + fn test_rubber_duck() { + assert_ne!("rubber", "duck"); + } +} \ No newline at end of file diff --git a/rust-maven-example/src/test/java/io/questdb/example/rust/BinaryTest.java b/rust-maven-example/src/test/java/io/questdb/example/rust/BinaryTest.java index 0360b6f..67be8b6 100644 --- a/rust-maven-example/src/test/java/io/questdb/example/rust/BinaryTest.java +++ b/rust-maven-example/src/test/java/io/questdb/example/rust/BinaryTest.java @@ -24,16 +24,18 @@ package io.questdb.example.rust; -import static org.junit.Assert.assertEquals; +import org.junit.Test; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import org.junit.Test; +import static org.junit.Assert.assertEquals; public class BinaryTest { @@ -52,6 +54,11 @@ public void testBinary() throws Exception { .collect(Collectors.toList()); assertEquals(0, process.waitFor()); - assertEquals(Arrays.asList("!dlroW olleH"), output); + final List exp = new ArrayList<>(); + Collections.addAll(exp, + ">>>>>>>>>>>>>>>>>>>>>>>>>", + "!dlroW olleH", + "<<<<<<<<<<<<<<<<<<<<<<<<<<"); + assertEquals(exp, output); } } diff --git a/rust-maven-example/src/test/java/io/questdb/example/rust/LibTest.java b/rust-maven-example/src/test/java/io/questdb/example/rust/LibTest.java index 9772c5f..d091935 100644 --- a/rust-maven-example/src/test/java/io/questdb/example/rust/LibTest.java +++ b/rust-maven-example/src/test/java/io/questdb/example/rust/LibTest.java @@ -24,10 +24,10 @@ package io.questdb.example.rust; -import static org.junit.Assert.assertEquals; - import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class LibTest { @Test diff --git a/rust-maven-plugin/pom.xml b/rust-maven-plugin/pom.xml index e34f39e..6e2ab63 100644 --- a/rust-maven-plugin/pom.xml +++ b/rust-maven-plugin/pom.xml @@ -35,8 +35,7 @@ Rust Maven Plugin Embed Rust Cargo projects in Java - - https://github.com/questdb/rust-maven-project + https://github.com/questdb/rust-maven-plugin The Apache Software License, Version 2.0 @@ -44,12 +43,30 @@ repo + + + QuestDB Team + hello@questdb.io + QuestDB + https://questdb.io/ + + - https://github.com/questdb/rust-maven-project - scm:git:https://github.com/questdb/rust-maven-project.git - scm:git:git@github.com:questdb/rust-maven-project.git + https://github.com/questdb/rust-maven-plugin + scm:git:https://github.com/questdb/rust-maven-plugin.git + scm:git:git@github.com:questdb/rust-maven-plugin.git HEAD + + + central + https://oss.sonatype.org/content/repositories/snapshots + + + central + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + 1.8 @@ -59,6 +76,11 @@ + + io.questdb + jar-jni + 1.0.0-SNAPSHOT + org.apache.maven maven-plugin-api @@ -78,9 +100,9 @@ provided - com.moandjiezana.toml - toml4j - 0.7.2 + org.tomlj + tomlj + 1.1.0 junit diff --git a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoBuildMojo.java b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoBuildMojo.java index 532d2ab..e749c34 100644 --- a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoBuildMojo.java +++ b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoBuildMojo.java @@ -24,248 +24,60 @@ package io.questdb.maven.rust; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.project.MavenProject; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.Executors; +import java.nio.file.Path; +import java.nio.file.Paths; /** * An example of a Maven plugin. */ @Mojo(name = "build", defaultPhase = LifecyclePhase.COMPILE, threadSafe = true) -public class CargoBuildMojo extends AbstractMojo { - - @Parameter(property = "project", readonly = true) - protected MavenProject project; - - @Parameter(property = "environmentVariables") - HashMap environmentVariables; - - /** - * Path to the `cargo` command. If unset or set to "cargo", uses $PATH. - */ - @Parameter(property = "cargoPath", defaultValue = "cargo") - private String cargoPath; - - /** - * Path to the Rust crate (or workspace) to build. - */ - @Parameter(property = "path", required = true) - private String path; - - /** - * Build artifacts in release mode, with optimizations. - * Defaults to "false" and creates a debug build. - * Equivalent to Cargo's `--release` option. - */ - @Parameter(property = "release", defaultValue = "false") - private boolean release; - - /** - * List of features to activate. - * If not specified, default features are activated. - * Equivalent to Cargo's `--features` option. - */ - @Parameter(property = "features") - private String[] features; - - /** - * Activate all available features. - * Defaults to "false". - * Equivalent to Cargo's `--all-features` option. - */ - @Parameter(property = "all-features", defaultValue = "false") - private boolean allFeatures; - - /** - * Do not activate the `default` feature. - * Defaults to "false". - * Equivalent to Cargo's `--no-default-features` option. - */ - @Parameter(property = "no-default-features", defaultValue = "false") - private boolean noDefaultFeatures; - - /** - * Build all tests. - * Defaults to "false". - * Equivalent to Cargo's `--tests` option. - */ - @Parameter(property = "tests", defaultValue = "false") - private boolean tests; - - /** - * Additional args to pass to cargo. - */ - @Parameter(property = "extra-args") - private String[] extraArgs; - +public class CargoBuildMojo extends CargoMojoBase { /** * Location to copy the built Rust binaries to. * If unset, the binaries are not copied and remain in the target directory. - * + *

* See also `copyWithPlatformDir`. */ @Parameter(property = "copyTo") private String copyTo; /** - * Further nest copy into a subdirectory named through the following expression: - * (System.getProperty("os.name") + "_" + System.getProperty("os.arch")).toLowerCase(); - * + * Further nest copy into a child directory named through the target's platform. + * The computed name matches that of the `io.questdb.jar.jni.OsInfo.platform()` method. + *

* See also `copyTo`. */ @Parameter(property = "copyWithPlatformDir") private boolean copyWithPlatformDir; - private String getCargoPath() { - String path = cargoPath; - - final boolean isWindows = System.getProperty("os.name") - .toLowerCase().startsWith("windows"); - - // Expand "~" to user's home directory. - // This works around a limitation of ProcessBuilder. - if (!isWindows && cargoPath.startsWith("~/")) { - path = System.getProperty("user.home") + cargoPath.substring(1); - } - - return path; - } - - private File getPath() { - File file = new File(path); - if (file.isAbsolute()) { - return file; - } else { - return new File(project.getBasedir(), path); - } - } - - private String getName() { - final String[] components = path.split("/"); - return components[components.length - 1]; - } - - private File getTargetDir() { - return new File(new File(new File(project.getBuild().getDirectory()), "rust-maven-plugin"), getName()); - } - - private void runCommand(List args) - throws IOException, InterruptedException, MojoExecutionException { - final ProcessBuilder processBuilder = new ProcessBuilder(args); - processBuilder.redirectErrorStream(true); - processBuilder.environment().putAll(environmentVariables); - - // Set the current working directory for the cargo command. - processBuilder.directory(getPath()); - final Process process = processBuilder.start(); - Log log = getLog(); - Executors.newSingleThreadExecutor().submit(() -> { - new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines() - .forEach(log::info); - }); - - final int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new MojoExecutionException("Cargo command failed with exit code " + exitCode); - } - } - - private void cargo(List args) throws MojoExecutionException { - String cargoPath = getCargoPath(); - final List cmd = new ArrayList<>(); - cmd.add(cargoPath); - cmd.addAll(args); - getLog().info("Working directory: " + getPath()); - if (!environmentVariables.isEmpty()) { - getLog().info("Environment variables:"); - for (String key : environmentVariables.keySet()) { - getLog().info(" " + key + "=" + Shlex.quote(environmentVariables.get(key))); - } - } - getLog().info("Running: " + Shlex.quote(cmd)); - try { - runCommand(cmd); - } catch (IOException | InterruptedException e) { - CargoInstalledChecker.INSTANCE.check(getLog(), cargoPath); - throw new MojoExecutionException("Failed to invoke cargo", e); - } + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + final Crate crate = new Crate( + getCrateRoot(), + getTargetRootDir(), + extractCrateParams()); + crate.setLog(getLog()); + crate.build(); + crate.copyArtifacts(); } - private File getCopyToDir() throws MojoExecutionException { - File copyToDir = new File(copyTo); - if (!copyToDir.isAbsolute()) { - copyToDir = new File(project.getBasedir(), copyTo); - } - if (copyWithPlatformDir) { - final String osName = System.getProperty("os.name").toLowerCase(); - final String osArch = System.getProperty("os.arch").toLowerCase(); - final String platform = (osName + "-" + osArch).replace(' ', '_'); - copyToDir = new File(copyToDir, platform); - } - if (!copyToDir.exists()) { - if (!copyToDir.mkdirs()) { - throw new MojoExecutionException("Failed to create directory " + copyToDir); + private Crate.Params extractCrateParams() throws MojoExecutionException { + final Crate.Params params = getCommonCrateParams(); + if (copyTo != null) { + Path copyToDir = Paths.get(copyTo); + if (!copyToDir.isAbsolute()) { + copyToDir = project.getBasedir().toPath() + .resolve(copyToDir); } + params.copyToDir = copyToDir; } - if (!copyToDir.isDirectory()) { - throw new MojoExecutionException(copyToDir + " is not a directory"); - } - return copyToDir; - } - - @Override - public void execute() throws MojoExecutionException { - List args = new ArrayList<>(); - args.add("build"); - - args.add("--target-dir"); - args.add(getTargetDir().getAbsolutePath()); - - if (release) { - args.add("--release"); - } - - if (allFeatures) { - args.add("--all-features"); - } - - if (noDefaultFeatures) { - args.add("--no-default-features"); - } - - if (features != null && features.length > 0) { - args.add("--features"); - args.add(String.join(",", features)); - } - - if (tests) { - args.add("--tests"); - } - - if (extraArgs != null) { - Collections.addAll(args, extraArgs); - } - cargo(args); - - // if/when --out-dir is stabilized then the outputRedirector function should be replaced with just the following args: - // args.add("--out-dir") - // args.add(getCopyToDir()); - - new ManualOutputRedirector(getLog(), getName(), release, getPath(), getTargetDir(), getCopyToDir()).copyArtifacts(); + params.copyWithPlatformDir = copyWithPlatformDir; + return params; } } diff --git a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoMojoBase.java b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoMojoBase.java new file mode 100644 index 0000000..61e14dc --- /dev/null +++ b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoMojoBase.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.maven.rust; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; + + +public abstract class CargoMojoBase extends AbstractMojo { + @Parameter(property = "project", readonly = true) + protected MavenProject project; + + @Parameter(property = "environmentVariables") + private HashMap environmentVariables; + + /** + * Path to the `cargo` command. If unset or set to "cargo", uses $PATH. + */ + @Parameter(property = "cargoPath", defaultValue = "cargo") + private String cargoPath; + + /** + * Path to the Rust crate to build. + */ + @Parameter(property = "path", required = true) + private String path; + + /** + * Build artifacts in release mode, with optimizations. + * Defaults to "false" and creates a debug build. + * Equivalent to Cargo's `--release` option. + */ + @Parameter(property = "release", defaultValue = "false") + private boolean release; + + /** + * List of features to activate. + * If not specified, default features are activated. + * Equivalent to Cargo's `--features` option. + */ + @Parameter(property = "features") + private String[] features; + + /** + * Activate all available features. + * Defaults to "false". + * Equivalent to Cargo's `--all-features` option. + */ + @Parameter(property = "all-features", defaultValue = "false") + private boolean allFeatures; + + /** + * Do not activate the `default` feature. + * Defaults to "false". + * Equivalent to Cargo's `--no-default-features` option. + */ + @Parameter(property = "no-default-features", defaultValue = "false") + private boolean noDefaultFeatures; + + /** + * Set the verbosity level, forwarded to Cargo. + * Valid values are "", "-q", "-v", "-vv". + */ + @Parameter(property = "verbosity") + private String verbosity; + + /** + * Additional args to pass to cargo. + */ + @Parameter(property = "extra-args") + private String[] extraArgs; + + protected String getVerbosity() throws MojoExecutionException { + if (verbosity == null) { + return null; + } + switch (verbosity) { + case "": + return null; + case "-q": + case "-v": + case "-vv": + return verbosity; + default: + throw new MojoExecutionException("Invalid verbosity: " + verbosity); + } + } + + protected Path getCrateRoot() { + Path crateRoot = Paths.get(path); + if (!crateRoot.isAbsolute()) { + crateRoot = project.getBasedir().toPath().resolve(path); + } + return crateRoot; + } + + protected Path getTargetRootDir() { + return Paths.get( + project.getBuild().getDirectory(), + "rust-maven-plugin"); + } + + protected Crate.Params getCommonCrateParams() throws MojoExecutionException { + final Crate.Params params = new Crate.Params(); + params.verbosity = getVerbosity(); + params.environmentVariables = environmentVariables; + params.cargoPath = cargoPath; + params.release = release; + params.features = features; + params.allFeatures = allFeatures; + params.noDefaultFeatures = noDefaultFeatures; + params.extraArgs = extraArgs; + return params; + } +} diff --git a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoTestMojo.java b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoTestMojo.java new file mode 100644 index 0000000..8ba7511 --- /dev/null +++ b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/CargoTestMojo.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.maven.rust; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +@Mojo(name = "test", defaultPhase = LifecyclePhase.TEST, threadSafe = true) +public class CargoTestMojo extends CargoMojoBase { + /** + * Skips running tests when building with `mvn package -DskipTests=true`. + */ + @Parameter(property = "skipTests", defaultValue = "false") + private boolean skipTests; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skipTests) { + getLog().info("Skipping tests"); + return; + } + final Crate crate = new Crate( + getCrateRoot(), + getTargetRootDir(), + getCommonCrateParams()); + crate.setLog(getLog()); + crate.test(); + } +} diff --git a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/Crate.java b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/Crate.java new file mode 100644 index 0000000..125d4f9 --- /dev/null +++ b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/Crate.java @@ -0,0 +1,501 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2023 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.maven.rust; + +import io.questdb.jar.jni.OsInfo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.tomlj.Toml; +import org.tomlj.TomlArray; +import org.tomlj.TomlInvalidTypeException; +import org.tomlj.TomlTable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executors; + +/** + * Controls running tasks on a Rust crate. + */ +public class Crate { + private final Path crateRoot; + private final Path targetDir; + private final Params params; + private final TomlTable cargoToml; + private final String packageName; + private Log log; + + public Crate( + Path crateRoot, + Path targetRootDir, + Params params) throws MojoExecutionException { + this.log = nullLog(); + this.crateRoot = crateRoot; + this.targetDir = targetRootDir.resolve(getDirName()); + this.params = params; + + final Path tomlPath = crateRoot.resolve("Cargo.toml"); + if (!Files.exists(tomlPath, LinkOption.NOFOLLOW_LINKS)) { + throw new MojoExecutionException( + "Cargo.toml file expected under: " + crateRoot); + } + try { + this.cargoToml = Toml.parse(tomlPath); + } catch (IOException e) { + throw new MojoExecutionException( + "Failed to parse Cargo.toml file: " + e.getMessage()); + } + + try { + packageName = cargoToml.getString("package.name"); + if (packageName == null) { + throw new MojoExecutionException( + "Missing required `package.name` from Cargo.toml file"); + } + } catch (TomlInvalidTypeException e) { + throw new MojoExecutionException( + "Failed to extract `package.name` from Cargo.toml file: " + + e.getMessage()); + } + } + + public static String pinLibName(String name) { + return OsInfo.LIB_PREFIX + + name.replace('-', '_') + + OsInfo.LIB_SUFFIX; + } + + public static String pinBinName(String name) { + return name + OsInfo.EXE_SUFFIX; + } + + public static Log nullLog() { + return new Log() { + @Override + public void debug(CharSequence content) { + } + + @Override + public void debug(CharSequence content, Throwable error) { + } + + @Override + public void debug(Throwable error) { + } + + @Override + public void error(CharSequence content) { + } + + @Override + public void error(CharSequence content, Throwable error) { + } + + @Override + public void error(Throwable error) { + } + + @Override + public void info(CharSequence content) { + } + + @Override + public void info(CharSequence content, Throwable error) { + } + + @Override + public void info(Throwable error) { + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(CharSequence content) { + } + + @Override + public void warn(CharSequence content, Throwable error) { + } + + @Override + public void warn(Throwable error) { + } + }; + } + + public void setLog(Log log) { + this.log = log; + } + + private String getDirName() { + return crateRoot.getFileName().toString(); + } + + private String getProfile() { + return params.release ? "release" : "debug"; + } + + public boolean hasCdylib() { + try { + TomlArray crateTypes = cargoToml.getArray("lib.crate-type"); + if (crateTypes == null) { + return false; + } + + for (int index = 0; index < crateTypes.size(); index++) { + String crateType = crateTypes.getString(index); + if ((crateType != null) && crateType.equals("cdylib")) { + return true; + } + } + + return false; + } catch (TomlInvalidTypeException e) { + return false; + } + } + + private String getCdylibName() throws MojoExecutionException { + String name = null; + try { + name = cargoToml.getString("lib.name"); + } catch (TomlInvalidTypeException e) { + throw new MojoExecutionException( + "Failed to extract `lib.name` from Cargo.toml file: " + + e.getMessage()); + } + + // The name might be missing, but the lib section might be present. + if ((name == null) && hasCdylib()) { + name = packageName; + } + + return name; + } + + private List getBinNames() throws MojoExecutionException { + final List binNames = new java.util.ArrayList<>(); + + String defaultBin = null; + if (Files.exists(crateRoot.resolve("src").resolve("main.rs"))) { + // Expecting default bin, given that there's no lib. + defaultBin = packageName; + binNames.add(defaultBin); + } + + TomlArray bins; + try { + bins = cargoToml.getArray("bin"); + } catch (TomlInvalidTypeException e) { + throw new MojoExecutionException( + "Failed to extract `bin`s from Cargo.toml file: " + + e.getMessage()); + } + + if (bins == null) { + return binNames; + } + + for (int index = 0; index < bins.size(); ++index) { + final TomlTable bin = bins.getTable(index); + if (bin == null) { + throw new MojoExecutionException( + "Failed to extract `bin`s from Cargo.toml file: " + + "expected a `bin` table at index " + index); + } + + String name = null; + try { + name = bin.getString("name"); + } catch (TomlInvalidTypeException e) { + throw new MojoExecutionException( + "Failed to extract `bin`s from Cargo.toml file: " + + "expected a string at index " + index + " `name` key"); + } + + if (name == null) { + throw new MojoExecutionException( + "Failed to extract `bin`s from Cargo.toml file: " + + "missing `name` key at `bin` with index " + index); + } + + String path = null; + try { + path = bin.getString("path"); + } catch (TomlInvalidTypeException e) { + throw new MojoExecutionException( + "Failed to extract `bin`s from Cargo.toml file: " + + "expected a string at index " + index + " `path` key"); + } + + // Handle special case where the default bin is renamed. + if ((path != null) && path.equals("src/main.rs")) { + defaultBin = name; + binNames.remove(0); + binNames.add(0, defaultBin); + } + + // This `[[bin]]` entry just configures the default bin. + // It's already been added. + if (!name.equals(defaultBin)) { + binNames.add(name); + } + } + + return binNames; + } + + public List getArtifactPaths() throws MojoExecutionException { + List paths = new ArrayList<>(); + final String profile = getProfile(); + + final String libName = getCdylibName(); + if (libName != null) { + final Path libPath = targetDir + .resolve(profile) + .resolve(pinLibName(libName)); + paths.add(libPath); + } + + for (String binName : getBinNames()) { + final Path binPath = targetDir + .resolve(profile) + .resolve(pinBinName(binName)); + paths.add(binPath); + } + + return paths; + } + + private String getCargoPath() { + String path = params.cargoPath; + + final boolean isWindows = System.getProperty("os.name") + .toLowerCase().startsWith("windows"); + + // Expand "~" to user's home directory. + // This works around a limitation of ProcessBuilder. + if (!isWindows && path.startsWith("~/")) { + path = System.getProperty("user.home") + path.substring(1); + } + + return path; + } + + private void runCommand(List args) + throws IOException, InterruptedException, MojoExecutionException { + final ProcessBuilder processBuilder = new ProcessBuilder(args); + processBuilder.redirectErrorStream(true); + processBuilder.environment().putAll(params.environmentVariables); + + // Set the current working directory for the cargo command. + processBuilder.directory(crateRoot.toFile()); + final Process process = processBuilder.start(); + Executors.newSingleThreadExecutor().submit(() -> { + new BufferedReader(new InputStreamReader(process.getInputStream())) + .lines() + .forEach(log::info); + }); + + final int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new MojoExecutionException( + "Cargo command failed with exit code " + exitCode); + } + } + + private void cargo(List args) throws MojoExecutionException, MojoFailureException { + String cargoPath = getCargoPath(); + final List cmd = new ArrayList<>(); + cmd.add(cargoPath); + cmd.addAll(args); + log.info("Working directory: " + crateRoot); + if (!params.environmentVariables.isEmpty()) { + log.info("Environment variables:"); + for (String key : params.environmentVariables.keySet()) { + log.info(" " + key + "=" + Shlex.quote( + params.environmentVariables.get(key))); + } + } + log.info("Running: " + Shlex.quote(cmd)); + try { + runCommand(cmd); + } catch (IOException | InterruptedException e) { + CargoInstalledChecker.INSTANCE.check(log, cargoPath); + throw new MojoFailureException("Failed to invoke cargo", e); + } + } + + private void addCargoArgs(List args) { + if (params.verbosity != null) { + args.add(params.verbosity); + } + + args.add("--target-dir"); + args.add(targetDir.toAbsolutePath().toString()); + + if (params.release) { + args.add("--release"); + } + + if (params.allFeatures) { + args.add("--all-features"); + } + + if (params.noDefaultFeatures) { + args.add("--no-default-features"); + } + + if (params.features != null && params.features.length > 0) { + args.add("--features"); + args.add(String.join(",", params.features)); + } + + if (params.tests) { + args.add("--tests"); + } + + if (params.extraArgs != null) { + Collections.addAll(args, params.extraArgs); + } + } + + public void build() throws MojoExecutionException, MojoFailureException { + List args = new ArrayList<>(); + args.add("build"); + addCargoArgs(args); + cargo(args); + } + + public void test() throws MojoExecutionException, MojoFailureException { + List args = new ArrayList<>(); + args.add("test"); + addCargoArgs(args); + cargo(args); + } + + private Path resolveCopyToDir() throws MojoExecutionException { + + Path copyToDir = params.copyToDir; + + if (copyToDir == null) { + return null; + } + + if (params.copyWithPlatformDir) { + copyToDir = copyToDir.resolve(OsInfo.PLATFORM); + } + + if (!Files.exists(copyToDir, LinkOption.NOFOLLOW_LINKS)) { + try { + Files.createDirectories(copyToDir); + } catch (IOException e) { + throw new MojoExecutionException( + "Failed to create directory " + copyToDir + + ": " + e.getMessage(), e); + } + } + + if (!Files.isDirectory(copyToDir)) { + throw new MojoExecutionException(copyToDir + " is not a directory"); + } + return copyToDir; + } + + public void copyArtifacts() throws MojoExecutionException { + // Cargo nightly has support for `--out-dir` + // which allows us to copy the artifacts directly to the desired path. + // Once the feature is stabilized, copy the artifacts directly via: + // args.add("--out-dir") + // args.add(resolveCopyToDir()); + final Path copyToDir = resolveCopyToDir(); + if (copyToDir == null) { + return; + } + final List artifactPaths = getArtifactPaths(); + log.info( + "Copying " + getDirName() + + "'s artifacts to " + Shlex.quote( + copyToDir.toAbsolutePath().toString())); + + for (Path artifactPath : artifactPaths) { + final Path fileName = artifactPath.getFileName(); + final Path destPath = copyToDir.resolve(fileName); + try { + Files.copy( + artifactPath, + destPath, + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new MojoExecutionException( + "Failed to copy " + artifactPath + + " to " + copyToDir + ":" + e.getMessage()); + } + log.info("Copied " + Shlex.quote(fileName.toString())); + } + } + + public static class Params { + public String verbosity; + public HashMap environmentVariables; + public String cargoPath; + public boolean release; + public String[] features; + public boolean allFeatures; + public boolean noDefaultFeatures; + public boolean tests; + public String[] extraArgs; + public Path copyToDir; + public boolean copyWithPlatformDir; + } +} diff --git a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/ManualOutputRedirector.java b/rust-maven-plugin/src/main/java/io/questdb/maven/rust/ManualOutputRedirector.java deleted file mode 100644 index 9f9a6f7..0000000 --- a/rust-maven-plugin/src/main/java/io/questdb/maven/rust/ManualOutputRedirector.java +++ /dev/null @@ -1,165 +0,0 @@ -/******************************************************************************* - * ___ _ ____ ____ - * / _ \ _ _ ___ ___| |_| _ \| __ ) - * | | | | | | |/ _ \/ __| __| | | | _ \ - * | |_| | |_| | __/\__ \ |_| |_| | |_) | - * \__\_\\__,_|\___||___/\__|____/|____/ - * - * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2023 QuestDB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ******************************************************************************/ - -package io.questdb.maven.rust; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; - -import com.moandjiezana.toml.Toml; - -final class ManualOutputRedirector { - - private final Log log; - private final String name; - private final boolean release; - private final File path; - private final File targetDir; - private final File copyToDir; - - ManualOutputRedirector(Log log, String name, boolean release, File path, File targetDir, File copyToDir) { - this.log = log; - this.name = name; - this.release = release; - this.path = path; - this.targetDir = targetDir; - this.copyToDir = copyToDir; - } - - void copyArtifacts() throws MojoExecutionException { - if (copyToDir == null) { - log.info("Not copying artifacts is not set"); - return; - } - - log.info("Copying " + name + "'s artifacts to " + Shlex.quote(copyToDir.getAbsolutePath())); - - final File tomlFile = new File(path, "Cargo.toml"); - if (!tomlFile.exists()) { - throw new MojoExecutionException("The arg might be incorrect. Cargo.toml file expected under: " + path); - } - final Toml toml = new Toml().read(tomlFile); - - final Set artifacts = getArtifacts(toml); - for (File artifact : artifacts) { - final File copyToPath = new File(copyToDir, artifact.getName()); - try { - Files.copy(artifact.toPath(), copyToPath.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new MojoExecutionException("Failed to copy " + artifact + " to " + copyToPath + ":" + e.getMessage()); - } - log.info("Copied " + Shlex.quote(artifact.getName())); - } - } - - Set getArtifacts(Toml toml) throws MojoExecutionException { - final String buildType = release ? "release" : "debug"; - - Toml mainPackage = toml.getTable("package"); - if (mainPackage == null) { - throw new MojoExecutionException("Malformed Cargo.toml file, expected a [package] section, but was missing."); - } - String packageName = mainPackage.getString("name", name); - - final Set artifacts = new HashSet<>(); - - // lib.rs or explicit [lib] - final File libraryPath = getLibraryPath(toml, packageName, buildType); - if (libraryPath != null) { - artifacts.add(libraryPath); - } - - // main.rs - final File defaultBinPath = new File(targetDir, buildType + "/" + packageName); - if (defaultBinPath.exists()) { - artifacts.add(defaultBinPath); - } - - // other [[bin]] - final List bins = toml.getTables("bin"); - if (bins != null) { - for (Toml bin : bins) { - final String binName = bin.getString("name"); - if (binName != null) { - final File binPath = new File(targetDir, buildType + "/" + binName); - if (binPath.exists()) { - artifacts.add(binPath); - } else { - throw new MojoExecutionException("Could not find expected binary: " + Shlex.quote(binPath.getAbsolutePath())); - } - } else { - throw new MojoExecutionException("Malformed Cargo.toml file, missing name in [[bin]] section"); - } - } - } - - if (artifacts.isEmpty()) { - throw new MojoExecutionException("Something went wrong. No artifacts produced. We expect a main.rs or specified [lib] or [[bin]] sections"); - } - - return artifacts; - } - - private File getLibraryPath(Toml toml, String packageName, String buildType) throws MojoExecutionException { - final Toml libSection = toml.getTable("lib"); - - // TODO: - // For now we only support system dynamic libraries (crate_type = cdylib). - // To make this more general, we need to parse the create_type from the .toml and check for --crate-type extra args. - // This gets even more complicated when you consider the dynamic create_types like: crate_type = "lib" - // Hopefully, --out-dir will allow us to avoid all this complexity in the future - final String libraryName = getOSspecificLibraryName( - libSection == null ? packageName : libSection.getString("name", packageName)); - final File libraryPath = new File(targetDir, buildType + "/" + libraryName); - - if (libraryPath.exists()) { - return libraryPath; - } else { - if (libSection == null) { - // assume no [lib] or lib.rs present, which is fine assuming this is a binary only crate - return null; - } else { - // if [lib] section is present then we expect library to be produced - throw new MojoExecutionException("Could not find expected library: " + Shlex.quote(libraryPath.getAbsolutePath())); - } - } - } - - static String getOSspecificLibraryName(String libName) { - final String osName = System.getProperty("os.name").toLowerCase(); - final String libPrefix = osName.startsWith("windows") ? "" : "lib"; - final String libSuffix = osName.startsWith("windows") - ? ".dll" : osName.contains("mac") - ? ".dylib" : ".so"; - return libPrefix + libName.replace("-", "_") + libSuffix; - } -} diff --git a/rust-maven-plugin/src/test/java/io/questdb/maven/rust/CrateTest.java b/rust-maven-plugin/src/test/java/io/questdb/maven/rust/CrateTest.java new file mode 100644 index 0000000..dbe9f41 --- /dev/null +++ b/rust-maven-plugin/src/test/java/io/questdb/maven/rust/CrateTest.java @@ -0,0 +1,511 @@ +package io.questdb.maven.rust; + +import io.questdb.jar.jni.OsInfo; +import org.apache.maven.plugin.MojoExecutionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.Assert.*; + + +public class CrateTest { + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + private Path targetRootDir; + + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("win"); + } + + public static boolean isMac() { + return System.getProperty("os.name").toLowerCase().contains("mac"); + } + + @Before + public void before() throws IOException { + tmpDir = new TemporaryFolder(); + tmpDir.create(); + targetRootDir = tmpDir.newFolder( + "target", + "rust-maven-plugin").toPath(); + } + + @After + public void after() { + tmpDir.delete(); + } + + private void doTestDefaultBin( + boolean release, + boolean copyTo, + boolean copyWithPlatformDir) throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test-bin-1", + release ? "release" : "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test-bin\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n"); + mock.touchSrc("main.rs"); + final Path mockBinPath = mock.touchBin("test-bin"); + assertTrue(Files.exists(mockBinPath)); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = release; + if (copyTo) { + params.copyToDir = tmpDir.newFolder("bin_dest_dir").toPath(); + } + params.copyWithPlatformDir = copyWithPlatformDir; + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(1, artifacts.size()); + assertEquals(mockBinPath, artifacts.get(0)); + + if (!copyTo) { + return; + } + + crate.copyArtifacts(); + + Path expectedBinPath = params.copyToDir; + if (copyWithPlatformDir) { + expectedBinPath = expectedBinPath.resolve( + OsInfo.PLATFORM); + } + expectedBinPath = expectedBinPath.resolve(mockBinPath.getFileName()); + + assertTrue(Files.exists(expectedBinPath)); + } + + public void testDefaultBinDebugNoCopyTo() throws Exception { + doTestDefaultBin(false, false, false); + } + + public void testDefaultBinReleaseNoCopyTo() throws Exception { + // Last arg to `true` should be ignored. + doTestDefaultBin(true, false, true); + } + + @Test + public void testDefaultBinDebugCopyTo() throws Exception { + doTestDefaultBin(false, true, false); + } + + @Test + public void testDefaultBinReleaseCopyTo() throws Exception { + doTestDefaultBin(true, true, false); + } + + @Test + public void testDefaultBinDebugCopyToPlatformDir() throws Exception { + doTestDefaultBin(false, true, true); + } + + @Test + public void testDefaultBinReleaseCopyToPlatformDir() throws Exception { + doTestDefaultBin(true, true, true); + } + + private void doTestCdylib(boolean release, boolean copyTo, boolean copyWithPlatformDir) throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test-lib-1", + release ? "release" : "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test-lib\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[lib]\n" + + "crate-type = [\"cdylib\"]\n"); + mock.touchSrc("lib.rs"); + final Path cdylibPath = mock.touchLib("test-lib"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = release; + if (copyTo) { + params.copyToDir = tmpDir.newFolder("lib_dest_dir").toPath(); + } + params.copyWithPlatformDir = copyWithPlatformDir; + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(1, artifacts.size()); + assertEquals(cdylibPath, artifacts.get(0)); + + if (!copyTo) { + return; + } + + crate.copyArtifacts(); + + Path expectedLibPath = params.copyToDir; + if (copyWithPlatformDir) { + expectedLibPath = expectedLibPath.resolve(OsInfo.PLATFORM); + } + expectedLibPath = expectedLibPath.resolve(cdylibPath.getFileName()); + + assertTrue(Files.exists(expectedLibPath)); + } + + @Test + public void testCdylibDebug() throws Exception { + // Last arg to `true` should be ignored. + doTestCdylib(false, false, true); + } + + @Test + public void testCdylibDebugCopyTo() throws Exception { + doTestCdylib(false, true, false); + } + + @Test + public void testCdylibReleaseCopyTo() throws Exception { + doTestCdylib(true, true, false); + } + + @Test + public void testCdylibDebugCopyToPlatformDir() throws Exception { + doTestCdylib(false, true, true); + } + + @Test + public void testCdylibReleaseCopyToPlatformDir() throws Exception { + doTestCdylib(true, true, true); + } + + @Test + public void testCustomCdylibName() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test-lib-1", + "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test-lib\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[lib]\n" + + "name = \"mylib\"\n" + + "crate-type = [\"cdylib\"]\n"); + mock.touchSrc("lib.rs"); + final Path cdylibPath = mock.touchLib("mylib"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = false; + params.copyToDir = tmpDir.newFolder("lib_dest_dir").toPath(); + params.copyWithPlatformDir = true; + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(1, artifacts.size()); + assertEquals(cdylibPath, artifacts.get(0)); + + crate.copyArtifacts(); + + Path expectedLibPath = params.copyToDir + .resolve(OsInfo.PLATFORM) + .resolve(cdylibPath.getFileName()); + + assertTrue(Files.exists(expectedLibPath)); + } + + @Test + public void testDefaultBinAndCdylib() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test42", + "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test42\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[lib]\n" + + "crate-type = [\"cdylib\"]\n"); + mock.touchSrc("lib.rs"); + mock.touchSrc("main.rs"); + final Path cdylibPath = mock.touchLib("test42"); + final Path binPath = mock.touchBin("test42"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = false; + params.copyToDir = tmpDir.newFolder("dest_dir").toPath(); + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(2, artifacts.size()); + assertEquals(cdylibPath, artifacts.get(0)); + assertEquals(binPath, artifacts.get(1)); + + crate.copyArtifacts(); + + Path expectedLibPath = params.copyToDir + .resolve(cdylibPath.getFileName()); + Path expectedBinPath = params.copyToDir + .resolve(binPath.getFileName()); + + assertTrue(Files.exists(expectedLibPath)); + assertTrue(Files.exists(expectedBinPath)); + } + + @Test + public void testRenamedDefaultBin() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test-custom-name-bin", + "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test-custom-name-bin\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[[bin]]\n" + + "name = \"test43\"\n" + + "path = \"src/main.rs\"\n"); + mock.touchSrc("main.rs"); + final Path binPath = mock.touchBin("test43"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = false; + params.copyToDir = tmpDir.newFolder("dest_dir").toPath(); + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(1, artifacts.size()); + assertEquals(binPath, artifacts.get(0)); + + crate.copyArtifacts(); + + Path expectedBinPath = params.copyToDir + .resolve(binPath.getFileName()); + + assertTrue(Files.exists(expectedBinPath)); + } + + @Test + public void testConfiguredDefaultBin() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate( + "test-configured-bin", + "debug"); + mock.writeCargoToml( + "[package]\n" + + "name = \"test-configured-bin\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[[bin]]\n" + + "name = \"test-configured-bin\"\n" + + "path = \"src/main.rs\"\n"); + mock.touchSrc("main.rs"); + final Path binPath = mock.touchBin("test-configured-bin"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = false; + params.copyToDir = tmpDir.newFolder("dest_dir").toPath(); + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(1, artifacts.size()); + assertEquals(binPath, artifacts.get(0)); + + crate.copyArtifacts(); + + Path expectedBinPath = params.copyToDir + .resolve(binPath.getFileName()); + + assertTrue(Files.exists(expectedBinPath)); + } + + @Test + public void testCdylibDefaultBinAndExplicitBin() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate("mixed", "release"); + mock.writeCargoToml( + "[package]\n" + + "name = \"mixed\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n" + + "\n" + + "[lib]\n" + + "crate-type = [\"cdylib\"]\n" + + "\n" + + "[[bin]]\n" + + "name = \"extra-bin\"\n" + + "path = \"src/extra-bin/main.rs\"\n"); + mock.touchSrc("lib.rs"); + mock.touchSrc("main.rs"); + mock.touchSrc("extra-bin", "main.rs"); + + final Path cdylibPath = mock.touchLib("mixed"); + final Path binPath = mock.touchBin("mixed"); + final Path extraBinPath = mock.touchBin("extra-bin"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = true; + params.copyToDir = tmpDir.newFolder("dest_dir").toPath(); + + final Crate crate = new Crate( + mock.crateRoot, + targetRootDir, + params); + + // Checking the expected paths it generates. + final List artifacts = crate.getArtifactPaths(); + assertEquals(3, artifacts.size()); + assertEquals(cdylibPath, artifacts.get(0)); + assertEquals(binPath, artifacts.get(1)); + assertEquals(extraBinPath, artifacts.get(2)); + + crate.copyArtifacts(); + + Path expectedLibPath = params.copyToDir + .resolve(cdylibPath.getFileName()); + Path expectedBinPath = params.copyToDir + .resolve(binPath.getFileName()); + Path expectedExtraBinPath = params.copyToDir + .resolve(extraBinPath.getFileName()); + + assertTrue(Files.exists(expectedLibPath)); + assertTrue(Files.exists(expectedBinPath)); + assertTrue(Files.exists(expectedExtraBinPath)); + } + + @Test + public void testBadCargoToml() throws Exception { + // Setting up mock Rust project directory. + final MockCrate mock = new MockCrate("bad-toml", "release"); + mock.writeCargoToml( + // "[package]\n" + MISSING! + "name = \"bad-toml\"\n" + + "version = \"0.1.0\"\n" + + "edition = \"2021\"\n"); + mock.touchSrc("main.rs"); + mock.touchBin("bad-toml"); + + // Configuring the build job. + final Crate.Params params = new Crate.Params(); + params.release = true; + params.copyToDir = tmpDir.newFolder("dest_dir").toPath(); + + assertThrows( + MojoExecutionException.class, + () -> new Crate(mock.crateRoot, targetRootDir, params)); + } + + class MockCrate { + private final String name; + private final String profile; + private final Path crateRoot; + + public MockCrate(String name, String profile) throws IOException { + this.name = name; + this.profile = profile; + this.crateRoot = tmpDir.newFolder(name).toPath(); + } + + public void writeCargoToml(String contents) throws IOException { + Path cargoToml = crateRoot.resolve("Cargo.toml"); + try (PrintWriter w = new PrintWriter(cargoToml.toFile(), "UTF-8")) { + w.write(contents); + } + } + + public Path touchBin(String name) throws IOException { + final Path mockBinPath = targetRootDir + .resolve(this.name) + .resolve(profile) + .resolve(name + (isWindows() ? ".exe" : "")); + if (!Files.exists(mockBinPath.getParent())) { + Files.createDirectories(mockBinPath.getParent()); + } + Files.createFile(mockBinPath); + return mockBinPath; + } + + public Path touchSrc(String... pathComponents) throws IOException { + Path srcPath = crateRoot.resolve("src"); + for (String pathComponent : pathComponents) { + srcPath = srcPath.resolve(pathComponent); + } + if (!Files.exists(srcPath.getParent())) { + Files.createDirectories(srcPath.getParent()); + } + Files.createFile(srcPath); + return srcPath; + } + + public Path touchLib(String name) throws IOException { + final String prefix = isWindows() ? "" : "lib"; + final String suffix = + isWindows() ? ".dll" : + isMac() ? ".dylib" + : ".so"; + final String libName = prefix + name.replace('-', '_') + suffix; + final Path libPath = targetRootDir + .resolve(this.name) + .resolve(profile) + .resolve(libName); + if (!Files.exists(libPath.getParent())) { + Files.createDirectories(libPath.getParent()); + } + Files.createFile(libPath); + return libPath; + } + } +} diff --git a/rust-maven-plugin/src/test/java/io/questdb/maven/rust/ManualOutputRedirectorTest.java b/rust-maven-plugin/src/test/java/io/questdb/maven/rust/ManualOutputRedirectorTest.java deleted file mode 100644 index c912d75..0000000 --- a/rust-maven-plugin/src/test/java/io/questdb/maven/rust/ManualOutputRedirectorTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package io.questdb.maven.rust; - -import static org.junit.Assert.*; - -import java.io.File; -import java.io.IOException; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.SystemStreamLog; -import org.codehaus.plexus.util.FileUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import com.google.common.collect.Sets; -import com.moandjiezana.toml.Toml; - -public class ManualOutputRedirectorTest { - - @Rule - public TemporaryFolder tempTargetDir = new TemporaryFolder(); - - @Before - public void before() throws IOException { - tempTargetDir.newFolder("debug"); - } - - @Test - public void testFindDefaultBin() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\""); - - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml)); - - File expected = tempTargetDir.newFile("debug/test"); - - assertEquals(Sets.newHashSet(expected), testObject.getArtifacts(toml)); - } - - @Test - public void testFindDefaultLib() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\""); - - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml)); - - File expected = tempTargetDir.newFile("debug/" + ManualOutputRedirector.getOSspecificLibraryName("test")); - - assertEquals(Sets.newHashSet(expected), testObject.getArtifacts(toml)); - } - - @Test - public void testFindDefaultLibWithName() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\"\n" - + "[lib]\n" - + "name = \"mylib\""); - - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml)); - - File expected = tempTargetDir.newFile("debug/" + ManualOutputRedirector.getOSspecificLibraryName("mylib")); - - assertEquals(Sets.newHashSet(expected), testObject.getArtifacts(toml)); - } - - @Test - public void testFindDefaultBinAndLibrary() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\""); - - File expectedBin = tempTargetDir.newFile("debug/test"); - File expectedLib = tempTargetDir.newFile("debug/" + ManualOutputRedirector.getOSspecificLibraryName("test")); - - assertEquals(Sets.newHashSet(expectedLib, expectedBin), testObject.getArtifacts(toml)); - } - - @Test - public void testFindDefaultBinExplicit() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\"\n" - + "[[bin]]\n" - + "name = \"test\"\n" - + "src = \"src/main.rs\""); - - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml)); - - File expected = tempTargetDir.newFile("debug/test"); - - assertEquals(Sets.newHashSet(expected), testObject.getArtifacts(toml)); - } - - @Test - public void testFindMixed() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - Toml toml = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\"\n" - + "[lib]\n" - + "name = \"mylib\"\n" - + "[[bin]]\n" - + "name = \"myexe1\"\n" - + "src = \"src/myexe1.rs\"\n" - + "[[bin]]\n" - + "name = \"myexe2\"\n" - + "src = \"src/myexe1.rs\""); - - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml)); - - File expectedLib = tempTargetDir.newFile("debug/" + ManualOutputRedirector.getOSspecificLibraryName("mylib")); - File expectedDefaultBin = tempTargetDir.newFile("debug/test"); - File expectedBin1 = tempTargetDir.newFile("debug/myexe1"); - File expectedBin2 = tempTargetDir.newFile("debug/myexe2"); - - // do not include default lib if [lib] section specified - tempTargetDir.newFile("debug/" + ManualOutputRedirector.getOSspecificLibraryName("test")); - - assertEquals(Sets.newHashSet(expectedLib, expectedDefaultBin, expectedBin1, expectedBin2), testObject.getArtifacts(toml)); - } - - @Test - public void testFindBadToml() throws Exception { - ManualOutputRedirector testObject = new ManualOutputRedirector(null, "dummy", false, null, tempTargetDir.getRoot(), null); - - // this should fail upstream, but for completeness test the bad Cargo.toml edge cases - - // missing [package] - Toml toml1 = new Toml().read("name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\""); - - tempTargetDir.newFile("debug/test"); - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml1)); - - // missing name in [[bin]] section - Toml toml2 = new Toml().read("[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\"\n" - + "[[bin]]\n" - + "src = \"src/myexe.rs\""); - - tempTargetDir.newFile("debug/myexe"); - assertThrows(MojoExecutionException.class, () -> testObject.getArtifacts(toml2)); - } - - @Test - public void testCopyArtifacts() throws Exception { - File copyToDir = tempTargetDir.newFolder("output"); - - ManualOutputRedirector testObject = new ManualOutputRedirector(new SystemStreamLog(), "dummy", true, tempTargetDir.getRoot(), tempTargetDir.getRoot(), copyToDir); - - File tomlFile = new File(tempTargetDir.getRoot(), "Cargo.toml"); - FileUtils.fileWrite(tomlFile, "[package]\n" - + "name = \"test\"\n" - + "version = \"0.1.0\"\n" - + "edition = \"2021\""); - - tempTargetDir.newFolder("release"); - File bin = tempTargetDir.newFile("release/test"); - - File expectedBin = new File(copyToDir, bin.getName()); - - assertFalse(expectedBin.exists()); - - testObject.copyArtifacts(); - - assertEquals(1, copyToDir.listFiles().length); - assertEquals(expectedBin, copyToDir.listFiles()[0]); - assertTrue(expectedBin.exists()); - } -}