├── .ci ├── deploy-snapshot.sh ├── gpg_keys.tar.enc ├── release.sh ├── update-archetypes.sh ├── update-gluon-samples.sh └── update-openjfx-docs.sh ├── .gitignore ├── .travis.settings.xml ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── openjfx │ │ ├── JavaFXBaseMojo.java │ │ ├── JavaFXJLinkMojo.java │ │ ├── JavaFXRunMojo.java │ │ └── model │ │ └── RuntimePathOption.java └── resources │ └── META-INF │ └── plexus │ └── components.xml └── test ├── java └── org │ └── openjfx │ ├── JavaFXBaseMojoTest.java │ ├── JavaFXRunMojoTestCase.java │ ├── MavenArtifactResolver.java │ ├── TestJavaFXRun0.java │ ├── TestJavaFXRun1.java │ └── TestLauncher.java └── resources └── unit ├── javafxrun-app-test └── pom.xml ├── javafxrun-basic-test └── pom.xml └── javafxrun-classpath-test ├── classpath-launcher.xml └── classpath-no-launcher.xml /.ci/deploy-snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find version 4 | ver=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) 5 | 6 | # deploy if snapshot found 7 | if [[ $ver == *"SNAPSHOT"* ]] 8 | then 9 | cp .travis.settings.xml $HOME/.m2/settings.xml 10 | mvn deploy -DskipTests=true -Dgpg.skip 11 | fi -------------------------------------------------------------------------------- /.ci/gpg_keys.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openjfx/javafx-maven-plugin/3b0520e0d26ae6b11398f872b82088129eb4077a/.ci/gpg_keys.tar.enc -------------------------------------------------------------------------------- /.ci/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit immediately if any command in the script fails 4 | set -e 5 | 6 | # Configure GIT 7 | git config --global user.name "Gluon Bot" 8 | git config --global user.email "githubbot@gluonhq.com" 9 | 10 | # Decrypt encrypted files 11 | openssl aes-256-cbc -K $encrypted_04317ab43744_key -iv $encrypted_04317ab43744_iv -in .ci/gpg_keys.tar.enc -out gpg_keys.tar -d 12 | if [[ ! -s gpg_keys.tar ]] 13 | then echo "Decryption failed." 14 | exit 1 15 | fi 16 | tar xvf gpg_keys.tar 17 | 18 | # Release artifacts 19 | cp .travis.settings.xml $HOME/.m2/settings.xml && mvn deploy -DskipTests=true -B -U -Prelease 20 | 21 | # Update version by 1 22 | newVersion=${TRAVIS_TAG%.*}.$((${TRAVIS_TAG##*.} + 1)) 23 | 24 | # Update README with the latest released version 25 | sed -i '// { 26 | :start 27 | N 28 | /<\/plugin>$/!b start 29 | /javafx-maven-plugin<\/artifactId>/ { 30 | /.*<\/version>/s//'"$TRAVIS_TAG"'<\/version>/ 31 | } 32 | }' README.md 33 | git commit README.md -m "Use latest release v$TRAVIS_TAG in README" 34 | 35 | # Update project version to next snapshot version 36 | mvn versions:set -DnewVersion=$newVersion-SNAPSHOT -DgenerateBackupPoms=false 37 | 38 | git commit pom.xml -m "Prepare development of $newVersion" 39 | git push https://gluon-bot:$GITHUB_PASSWORD@github.com/$TRAVIS_REPO_SLUG HEAD:master 40 | 41 | # Update archetypes 42 | bash .ci/update-archetypes.sh "$TRAVIS_TAG" 43 | 44 | # Update openjfx-docs 45 | bash .ci/update-openjfx-docs.sh "$TRAVIS_TAG" 46 | 47 | # Update gluon-samples 48 | bash .ci/update-gluon-samples.sh "$TRAVIS_TAG" -------------------------------------------------------------------------------- /.ci/update-archetypes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export REPO_NAME=javafx-maven-archetypes 4 | export REPO_SLUG=openjfx/javafx-maven-archetypes 5 | export XML_LOCATION=src/main/resources/META-INF/maven/archetype-metadata.xml 6 | 7 | cd $TRAVIS_BUILD_DIR 8 | git clone https://github.com/$REPO_SLUG 9 | cd $REPO_NAME 10 | 11 | # Traverse through all sub-directories starting with "javafx-archetype-" 12 | for f in ./javafx-archetype-* ; do 13 | # f is directory and not a symlink 14 | if [[ -d "$f" && ! -L "$f" ]]; then\ 15 | # Update for parent node with key='javafx-maven-plugin-version' 16 | xmlstarlet ed -P -L -u "//_:requiredProperty[@key='javafx-maven-plugin-version']/_:defaultValue" -v "$1" "$f"/$XML_LOCATION 17 | fi 18 | done 19 | 20 | git commit */$XML_LOCATION -m "Upgrade javafx-maven-plugin version to $1" 21 | git push https://gluon-bot:$GITHUB_PASSWORD@github.com/$REPO_SLUG HEAD:master -------------------------------------------------------------------------------- /.ci/update-gluon-samples.sh: -------------------------------------------------------------------------------- 1 | SAMPLES_REPO_SLUG=gluonhq/gluon-samples 2 | 3 | cd $TRAVIS_BUILD_DIR 4 | git clone https://github.com/$SAMPLES_REPO_SLUG 5 | cd gluon-samples 6 | 7 | # Update plugin version 8 | mvn versions:set-property -Dproperty=javafx.maven.plugin.version -DnewVersion="$1" -DgenerateBackupPoms=false 9 | 10 | git commit pom.xml -m "Update javafx-maven-plugin version to $1" 11 | git push https://gluon-bot:$GITHUB_PASSWORD@github.com/$SAMPLES_REPO_SLUG HEAD:master 12 | -------------------------------------------------------------------------------- /.ci/update-openjfx-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export REPO_NAME=openjfx-docs 4 | export REPO_SLUG=openjfx/openjfx-docs 5 | export VERSION_FILE_LOCATION=js/gluon.js 6 | 7 | cd $TRAVIS_BUILD_DIR 8 | git clone https://github.com/$REPO_SLUG 9 | cd $REPO_NAME 10 | 11 | # Update javafx-maven-plugin version 12 | sed -i "0,/JFX_MVN_PLUGIN_VERSION = \".*\"/s//JFX_MVN_PLUGIN_VERSION = \"$1\"/" $VERSION_FILE_LOCATION 13 | 14 | git commit $VERSION_FILE_LOCATION -m "Upgrade javafx-maven-plugin version to $1" 15 | git push https://gluon-bot:$GITHUB_PASSWORD@github.com/$REPO_SLUG HEAD:master -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target 3 | 4 | #IntelliJ IDEA 5 | *.iml 6 | .idea 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # VM Crash Files 12 | hs_err_pid* 13 | 14 | # Log file 15 | *.log 16 | /.settings 17 | /.project 18 | /.classpath 19 | -------------------------------------------------------------------------------- /.travis.settings.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | ossrh 6 | ${SONATYPE_USERNAME} 7 | ${SONATYPE_PASSWORD} 8 | 9 | 10 | 11 | 12 | 13 | ossrh 14 | 15 | true 16 | 17 | 18 | ${GPG_EXECUTABLE} 19 | ${GPG_KEYNAME} 20 | ${GPG_PASSPHRASE} 21 | 22 | false 23 | 24 | ${TRAVIS_BUILD_DIR}/pubring.gpg 25 | ${TRAVIS_BUILD_DIR}/secring.gpg 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # use java support 2 | language: java 3 | jdk: openjdk11 4 | 5 | # run in container 6 | sudo: false 7 | 8 | # use linux as operating system 9 | os: linux 10 | dist: xenial 11 | 12 | # configure xvfb screen 13 | services: 14 | - xvfb 15 | 16 | # Install xmlstarlet 17 | addons: 18 | apt: 19 | packages: 20 | - xmlstarlet 21 | 22 | # Do not run travis install step 23 | install: true 24 | 25 | # Assemble jars 26 | script: 27 | - mvn clean compile verify 28 | 29 | cache: 30 | directories: 31 | - $HOME/.m2 32 | 33 | deploy: 34 | # Deploy snapshots on every commit made to master 35 | - provider: script 36 | script: bash .ci/deploy-snapshot.sh 37 | skip_cleanup: true 38 | on: 39 | branch: master 40 | 41 | # Deploy releases on every tag push 42 | - provider: script 43 | script: bash .ci/release.sh 44 | skip_cleanup: true 45 | on: 46 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maven plugin for JavaFX 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/org.openjfx/javafx-maven-plugin.svg?color=%234DC71F)](https://search.maven.org/#search|ga|1|org.openjfx.javafx-maven-plugin) 4 | [![Travis CI](https://api.travis-ci.com/openjfx/javafx-maven-plugin.svg?branch=master)](https://travis-ci.com/openjfx/javafx-maven-plugin) 5 | [![Apache License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 6 | 7 | Maven plugin to run JavaFX 11+ applications 8 | 9 | ## Install 10 | 11 | The plugin is available via Maven Central. 12 | 13 | In case you want to build and install the latest snapshot, you can 14 | clone the project, set JDK 11 and run 15 | 16 | ``` 17 | mvn install 18 | ``` 19 | 20 | ## Usage 21 | 22 | Create a new Maven project, use an existing one like [HelloFX](https://github.com/openjfx/samples/tree/master/CommandLine/Modular/Maven/hellofx), or use an [archetype](https://github.com/openjfx/javafx-maven-archetypes). 23 | 24 | The project can be modular or non-modular. 25 | 26 | JavaFX dependencies are added as usual: 27 | 28 | ``` 29 | 30 | org.openjfx 31 | javafx-controls 32 | 12.0.2 33 | 34 | ``` 35 | 36 | Add the plugin: 37 | 38 | ``` 39 | 40 | org.openjfx 41 | javafx-maven-plugin 42 | 0.0.8 43 | 44 | hellofx/org.openjfx.App 45 | 46 | 47 | ``` 48 | 49 | Compile the project: 50 | 51 | ``` 52 | mvn compile 53 | ``` 54 | 55 | This step is optional and can be configured using the [maven-compiler-plugin](https://maven.apache.org/plugins/maven-compiler-plugin/). 56 | 57 | Run the project: 58 | 59 | ``` 60 | mvn javafx:run 61 | ``` 62 | 63 | For modular projects, create and run a custom image: 64 | 65 | ``` 66 | mvn javafx:jlink 67 | 68 | target/image/bin/java -m hellofx/org.openjfx.App 69 | ``` 70 | 71 | ### javafx:run options 72 | 73 | The plugin includes by default: `--module-path`, `--add-modules` and `-classpath` options. 74 | 75 | Optionally, the configuration can be modified with: 76 | 77 | - `mainClass`: The main class, fully qualified name, with or without module name 78 | - `workingDirectory`: The current working directory 79 | - `skip`: Skip the execution. Values: false (default), true 80 | - `outputFile`: File to redirect the process output 81 | - `options`: A list of VM options passed to the executable. 82 | - `commandlineArgs`: Arguments separated by space for the executed program 83 | - `includePathExceptionsInClasspath`: When resolving the module-path, setting this value to true will include the 84 | dependencies that generate path exceptions in the classpath. By default, the value is false, and these dependencies 85 | won't be included. 86 | - `runtimePathOption`: By default, the plugin will place *each* dependency either on modulepath or on classpath (based on certain factors). 87 | When `runtimePathOption` configuration is set, the plugin will place *all* the dependencies on either modulepath or classpath. 88 | 89 | If set as `MODULEPATH`, a module descriptor is required. All dependencies need to be either modularized or contain an Automatic-Module-Name. 90 | 91 | If set as `CLASSPATH`, a Launcher class ([like this one](https://github.com/openjfx/samples/blob/master/CommandLine/Non-modular/CLI/hellofx/src/hellofx/Launcher.java)) 92 | is required to run a JavaFX application. Also, if a module-info descriptor is present, it will be ignored. 93 | 94 | Values: MODULEPATH or CLASSPATH. 95 | 96 | This plugin supports Maven toolchains using the "jdk" tool. 97 | 98 | ### Example 99 | 100 | The following configuration adds some VM options, and a command line argument: 101 | 102 | ``` 103 | 104 | org.openjfx 105 | javafx-maven-plugin 106 | 0.0.8 107 | 108 | org.openjfx.hellofx/org.openjfx.App 109 | 110 | 111 | 112 | 113 | 114 | foo 115 | 116 | 117 | ``` 118 | 119 | When running maven with 120 | ``` 121 | mvn -Dbar=myBar javafx:run 122 | ``` 123 | it will be processed by the main method like: 124 | 125 | ```java 126 | public static void main(String[] args) { 127 | if (args.length > 0 && "foo".equals(args[0])) { 128 | // do something 129 | } 130 | if ("myBar".equals(System.getProperty("bar"))) { 131 | // do something 132 | } 133 | launch(); 134 | } 135 | ``` 136 | 137 | Note that the evaluation of `System.getProperty("bar")` can happen in any other place in the code. 138 | 139 | **Note** 140 | 141 | It is possible to use a local SDK instead of Maven Central. 142 | This is helpful for developers trying to test a local build of OpenJFX. 143 | Since transitive dependencies are not resolved, 144 | all the required jars needs to be added as a separate dependency, like: 145 | 146 | ``` 147 | 148 | /path/to/javafx-sdk 149 | 150 | 151 | 152 | 153 | org.openjfx 154 | javafx.base 155 | 1.0 156 | system 157 | ${sdk}/lib/javafx.base.jar 158 | 159 | ... 160 | 161 | ``` 162 | 163 | ### javafx:jlink options 164 | 165 | The same command line options for `jlink` can be set: 166 | 167 | - `stripDebug`: Strips debug information out. Values: false (default) or true 168 | - `stripJavaDebugAttributes`: Strip Java debug attributes out (since Java 13), Values: false (default) or true 169 | - `compress`: Compression level of the resources being used. Values: 0 (default), 1, 2. 170 | - `noHeaderFiles`: Removes the `includes` directory in the resulting runtime image. Values: false (default) or true 171 | - `noManPages`: Removes the `man` directory in the resulting runtime image. Values: false (default) or true 172 | - `bindServices`: Adds the option to bind services. Values: false (default) or true 173 | - `ignoreSigningInformation`: Adds the option to ignore signing information. Values: false (default) or true 174 | - `jlinkVerbose`: Adds the verbose option. Values: false (default) or true 175 | - `launcher`: Adds a launcher script with the given name. 176 | - If `options` are defined, these will be passed to the launcher script as vm options. 177 | - If `commandLineArgs` are defined, these will be passed to the launcher script as command line arguments. 178 | - `jlinkImageName`: The name of the folder with the resulting runtime image 179 | - `jlinkZipName`: When set, creates a zip of the resulting runtime image 180 | - `jlinkExecutable`: The `jlink` executable. It can be a full path or the name of the executable, if it is in the PATH. 181 | - `jmodsPath`: When using a local JavaFX SDK, sets the path to the local JavaFX jmods 182 | 183 | For instance, with the following configuration: 184 | 185 | ``` 186 | 187 | org.openjfx 188 | javafx-maven-plugin 189 | 0.0.8 190 | 191 | true 192 | 2 193 | true 194 | true 195 | hellofx 196 | hello 197 | hellozip 198 | hellofx/org.openjfx.MainApp 199 | 200 | 201 | ``` 202 | 203 | A custom image can be created and run as: 204 | 205 | ``` 206 | mvn clean javafx:jlink 207 | 208 | target/hello/bin/hellofx 209 | ``` 210 | 211 | ## Issues and Contributions ## 212 | 213 | Issues can be reported to the [Issue tracker](https://github.com/openjfx/javafx-maven-plugin/issues/). 214 | 215 | Contributions can be submitted via [Pull requests](https://github.com/openjfx/javafx-maven-plugin/pulls/), 216 | providing you have signed the [Gluon Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/16aoFTmzs8lZTfiyrEm8YgMqMYaGQl0J8wA0VJE2LCCY). 217 | 218 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 4.0.0 20 | org.openjfx 21 | javafx-maven-plugin 22 | maven-plugin 23 | 0.0.9-SNAPSHOT 24 | javafx-maven-plugin Maven Mojo 25 | The JavaFX Plugin is used to run JavaFX 11+ projects 26 | 2019 27 | 28 | https://openjfx.io 29 | 30 | 31 | UTF-8 32 | 8 33 | 8 34 | 12.0.1 35 | 3.1.0 36 | 1.6 37 | 38 | 39 | 40 | 41 | org.apache.maven 42 | maven-plugin-api 43 | 3.6.0 44 | 45 | 46 | org.apache.maven.plugin-tools 47 | maven-plugin-annotations 48 | 3.6.0 49 | provided 50 | 51 | 52 | org.codehaus.plexus 53 | plexus-java 54 | 0.9.11 55 | 56 | 57 | org.codehaus.plexus 58 | plexus-archiver 59 | 3.6.0 60 | 61 | 62 | org.apache.commons 63 | commons-exec 64 | 1.3 65 | 66 | 67 | 68 | 69 | org.openjfx 70 | javafx-controls 71 | ${javafx.version} 72 | test 73 | 74 | 75 | org.apache.maven.plugin-testing 76 | maven-plugin-testing-harness 77 | 3.3.0 78 | test 79 | 80 | 81 | org.apache.maven 82 | maven-core 83 | 3.6.0 84 | 85 | 86 | org.apache.maven 87 | maven-compat 88 | 3.6.0 89 | test 90 | 91 | 92 | org.apache.maven 93 | maven-aether-provider 94 | 3.3.9 95 | test 96 | 97 | 98 | org.eclipse.aether 99 | aether-transport-http 100 | 1.1.0 101 | test 102 | 103 | 104 | org.eclipse.aether 105 | aether-transport-file 106 | 1.1.0 107 | test 108 | 109 | 110 | org.eclipse.aether 111 | aether-connector-basic 112 | 1.1.0 113 | test 114 | 115 | 116 | org.codehaus.plexus 117 | plexus-interpolation 118 | 1.25 119 | test 120 | 121 | 122 | org.mockito 123 | mockito-core 124 | 2.24.5 125 | test 126 | 127 | 128 | junit 129 | junit 130 | 4.13.1 131 | test 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-compiler-plugin 140 | 3.8.0 141 | 142 | ${maven.compiler.source} 143 | ${maven.compiler.target} 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-plugin-plugin 149 | 3.6.0 150 | 151 | 152 | org.sonatype.plugins 153 | nexus-staging-maven-plugin 154 | 1.6.7 155 | true 156 | 157 | ossrh 158 | https://oss.sonatype.org/ 159 | true 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | release 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-source-plugin 173 | 2.2.1 174 | 175 | 176 | attach-sources 177 | 178 | jar-no-fork 179 | 180 | 181 | 182 | 183 | 184 | org.apache.maven.plugins 185 | maven-javadoc-plugin 186 | ${javadoc.plugin.version} 187 | 188 | 189 | attach-javadocs 190 | 191 | jar 192 | 193 | 194 | 195 | 196 | 8 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-gpg-plugin 202 | ${gpg.plugin.version} 203 | 204 | 205 | sign-artifacts 206 | verify 207 | 208 | sign 209 | 210 | 211 | 212 | 213 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | Gluon 230 | https://www.gluonhq.com 231 | 232 | 233 | 234 | GitHub 235 | https://github.com/openjfx/javafx-maven-plugin/issues 236 | 237 | 238 | 239 | 240 | The Apache Software License, Version 2.0 241 | https://www.apache.org/licenses/LICENSE-2.0.txt 242 | repo 243 | 244 | 245 | 246 | 247 | 248 | Abhinay Agarwal 249 | abhinay.agarwal@gluonhq.com 250 | Gluon 251 | https://www.gluonhq.com 252 | 253 | 254 | José Pereda 255 | jose.pereda@gluonhq.com 256 | Gluon 257 | https://www.gluonhq.com 258 | 259 | 260 | 261 | 262 | https://github.com/openjfx/javafx-maven-plugin 263 | scm:git:git://github.com/openjfx/javafx-maven-plugin.git 264 | scm:git:ssh://git@github.com:openjfx/javafx-maven-plugin.git 265 | 266 | 267 | 268 | 269 | ossrh 270 | https://oss.sonatype.org/content/repositories/snapshots 271 | 272 | 273 | ossrh 274 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /src/main/java/org/openjfx/JavaFXBaseMojo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, 2020, Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | import static org.openjfx.model.RuntimePathOption.CLASSPATH; 20 | import static org.openjfx.model.RuntimePathOption.MODULEPATH; 21 | 22 | import java.io.BufferedOutputStream; 23 | import java.io.File; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.OutputStream; 27 | import java.net.MalformedURLException; 28 | import java.net.URL; 29 | import java.net.URLClassLoader; 30 | import java.nio.file.Files; 31 | import java.nio.file.Path; 32 | import java.nio.file.Paths; 33 | import java.util.ArrayList; 34 | import java.util.Arrays; 35 | import java.util.Collection; 36 | import java.util.HashMap; 37 | import java.util.LinkedHashMap; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.Objects; 41 | import java.util.Properties; 42 | import java.util.stream.Collectors; 43 | import java.util.stream.Stream; 44 | 45 | import org.apache.commons.exec.CommandLine; 46 | import org.apache.commons.exec.ExecuteException; 47 | import org.apache.commons.exec.ExecuteResultHandler; 48 | import org.apache.commons.exec.Executor; 49 | import org.apache.commons.exec.OS; 50 | import org.apache.commons.exec.ProcessDestroyer; 51 | import org.apache.commons.exec.PumpStreamHandler; 52 | import org.apache.commons.exec.ShutdownHookProcessDestroyer; 53 | import org.apache.maven.artifact.Artifact; 54 | import org.apache.maven.artifact.DependencyResolutionRequiredException; 55 | import org.apache.maven.execution.MavenSession; 56 | import org.apache.maven.plugin.AbstractMojo; 57 | import org.apache.maven.plugin.BuildPluginManager; 58 | import org.apache.maven.plugin.MojoExecutionException; 59 | import org.apache.maven.plugins.annotations.Component; 60 | import org.apache.maven.plugins.annotations.Parameter; 61 | import org.apache.maven.project.MavenProject; 62 | import org.apache.maven.toolchain.Toolchain; 63 | import org.apache.maven.toolchain.ToolchainManager; 64 | import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor; 65 | import org.codehaus.plexus.languages.java.jpms.LocationManager; 66 | import org.codehaus.plexus.languages.java.jpms.ModuleNameSource; 67 | import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest; 68 | import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult; 69 | import org.codehaus.plexus.util.StringUtils; 70 | import org.codehaus.plexus.util.cli.CommandLineUtils; 71 | import org.openjfx.model.RuntimePathOption; 72 | 73 | abstract class JavaFXBaseMojo extends AbstractMojo { 74 | 75 | private static final String JAVAFX_APPLICATION_CLASS_NAME = "javafx.application.Application"; 76 | static final String JAVAFX_PREFIX = "javafx"; 77 | 78 | @Parameter(defaultValue = "${project}", readonly = true) 79 | MavenProject project; 80 | 81 | @Parameter(defaultValue = "${session}", readonly = true) 82 | private MavenSession session; 83 | 84 | @Component 85 | private BuildPluginManager pluginManager; 86 | 87 | @Component 88 | private LocationManager locationManager; 89 | 90 | @Parameter(property = "javafx.mainClass", required = true) 91 | String mainClass; 92 | 93 | /** 94 | * Skip the execution. 95 | */ 96 | @Parameter(property = "javafx.skip", defaultValue = "false") 97 | boolean skip; 98 | 99 | @Parameter(readonly = true, required = true, defaultValue = "${basedir}") 100 | File basedir; 101 | 102 | @Parameter(readonly = true, required = true, defaultValue = "${project.build.directory}") 103 | File builddir; 104 | 105 | /** 106 | * Type of {@link RuntimePathOption} to run the application. 107 | */ 108 | @Parameter(property = "javafx.runtimePathOption") 109 | RuntimePathOption runtimePathOption; 110 | 111 | /** 112 | * The current working directory. Optional. If not specified, basedir will be used. 113 | */ 114 | @Parameter(property = "javafx.workingDirectory") 115 | File workingDirectory; 116 | 117 | @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true) 118 | private List compilePath; 119 | 120 | @Parameter(property = "javafx.outputFile") 121 | File outputFile; 122 | 123 | /** 124 | * If set to true the child process executes asynchronously and build execution continues in parallel. 125 | */ 126 | @Parameter(property = "javafx.async", defaultValue = "false") 127 | private boolean async; 128 | 129 | /** 130 | * If set to true, the asynchronous child process is destroyed upon JVM shutdown. If set to false, asynchronous 131 | * child process continues execution after JVM shutdown. Applies only to asynchronous processes; ignored for 132 | * synchronous processes. 133 | */ 134 | @Parameter(property = "javafx.asyncDestroyOnShutdown", defaultValue = "true") 135 | private boolean asyncDestroyOnShutdown; 136 | 137 | /** 138 | *

139 | * A list of vm options passed to the {@code executable}. 140 | *

141 | * 142 | */ 143 | @Parameter 144 | List options; 145 | 146 | /** 147 | * Arguments separated by space for the executed program. For example: "-j 20" 148 | */ 149 | @Parameter(property = "javafx.args") 150 | String commandlineArgs; 151 | 152 | /** 153 | * If set to true, it will include the dependencies that 154 | * generate path exceptions in the classpath. Default is false. 155 | */ 156 | @Parameter(property = "javafx.includePathExceptionsInClasspath", defaultValue = "false") 157 | private boolean includePathExceptionsInClasspath; 158 | 159 | /** 160 | * 161 | */ 162 | @Component 163 | private ToolchainManager toolchainManager; 164 | 165 | List classpathElements; 166 | List modulepathElements; 167 | Map pathElements; 168 | JavaModuleDescriptor moduleDescriptor; 169 | private ProcessDestroyer processDestroyer; 170 | 171 | static boolean isMavenUsingJava8() { 172 | return System.getProperty("java.version").startsWith("1.8"); 173 | } 174 | 175 | static boolean isTargetUsingJava8(CommandLine commandLine) { 176 | final String java = commandLine.getExecutable(); 177 | return java != null && Files.exists(Paths.get(java).resolve("../../jre/lib/rt.jar").normalize()); 178 | } 179 | 180 | void preparePaths(Path jdkHome) throws MojoExecutionException { 181 | if (project == null) { 182 | return; 183 | } 184 | 185 | String outputDirectory = project.getBuild().getOutputDirectory(); 186 | if (outputDirectory == null || outputDirectory.isEmpty()) { 187 | throw new MojoExecutionException("Error: Output directory doesn't exist"); 188 | } 189 | 190 | File[] classes = new File(outputDirectory).listFiles(); 191 | if (classes == null || classes.length == 0) { 192 | throw new MojoExecutionException("Error: Output directory is empty"); 193 | } 194 | 195 | File moduleDescriptorPath = Stream 196 | .of(classes) 197 | .filter(file -> "module-info.class".equals(file.getName())) 198 | .findFirst() 199 | .orElse(null); 200 | 201 | modulepathElements = new ArrayList<>(compilePath.size()); 202 | classpathElements = new ArrayList<>(compilePath.size()); 203 | pathElements = new LinkedHashMap<>(compilePath.size()); 204 | 205 | try { 206 | Collection dependencyArtifacts = getCompileClasspathElements(project); 207 | getLog().debug("Total dependencyArtifacts: " + dependencyArtifacts.size()); 208 | ResolvePathsRequest fileResolvePathsRequest = ResolvePathsRequest.ofFiles(dependencyArtifacts); 209 | 210 | getLog().debug("module descriptor path: " + moduleDescriptorPath); 211 | if (moduleDescriptorPath != null) { 212 | fileResolvePathsRequest.setMainModuleDescriptor(moduleDescriptorPath); 213 | } 214 | if (jdkHome != null) { 215 | fileResolvePathsRequest.setJdkHome(jdkHome.toFile()); 216 | } 217 | ResolvePathsResult resolvePathsResult = locationManager.resolvePaths(fileResolvePathsRequest); 218 | resolvePathsResult.getPathElements().forEach((key, value) -> pathElements.put(key.getPath(), value)); 219 | 220 | if (!resolvePathsResult.getPathExceptions().isEmpty()) { 221 | getLog().warn("There are " + resolvePathsResult.getPathExceptions().size() + " pathException(s). The related dependencies will be ignored."); 222 | resolvePathsResult.getPathExceptions().forEach((key, value) -> { 223 | String message = "Dependency: " + key; 224 | if (value != null) { 225 | message += "\n - exception: " + value.getMessage(); 226 | Throwable t = value.getCause(); 227 | if (t != null) { 228 | message += "\n - cause: " + t.getMessage(); 229 | } 230 | } 231 | getLog().warn(message); 232 | }); 233 | } 234 | 235 | if (runtimePathOption == MODULEPATH && moduleDescriptorPath == null) { 236 | throw new MojoExecutionException("module-info.java file is required for MODULEPATH runtimePathOption"); 237 | } 238 | 239 | if (moduleDescriptorPath != null) { 240 | if (!resolvePathsResult.getPathExceptions().isEmpty() && !isMavenUsingJava8()) { 241 | // for each path exception, show a warning to plugin user... 242 | for (Map.Entry pathException : resolvePathsResult.getPathExceptions().entrySet()) { 243 | Throwable cause = pathException.getValue(); 244 | while (cause.getCause() != null) { 245 | cause = cause.getCause(); 246 | } 247 | String fileName = pathException.getKey().getName(); 248 | getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage()); 249 | } 250 | // ...if includePathExceptionsInClasspath is NOT enabled; provide configuration hint to plugin user 251 | if (!includePathExceptionsInClasspath) { 252 | getLog().warn("Some dependencies encountered issues while attempting to be resolved as modules" + 253 | " and will not be included in the classpath; you can change this behavior via the " + 254 | " 'includePathExceptionsInClasspath' configuration parameter."); 255 | } 256 | } 257 | moduleDescriptor = createModuleDescriptor(resolvePathsResult); 258 | for (Map.Entry entry : resolvePathsResult.getModulepathElements().entrySet()) { 259 | if (ModuleNameSource.FILENAME.equals(entry.getValue())) { 260 | final String message = "Required filename-based automodules detected. " 261 | + "Please don't publish this project to a public artifact repository!"; 262 | 263 | if (moduleDescriptor != null && moduleDescriptor.exports().isEmpty()) { 264 | // application 265 | getLog().info(message); 266 | } else { 267 | // library 268 | getLog().warn(message); 269 | } 270 | break; 271 | } 272 | } 273 | resolvePathsResult.getClasspathElements().forEach(file -> classpathElements.add(file.getPath())); 274 | resolvePathsResult.getModulepathElements().keySet().forEach(file -> modulepathElements.add(file.getPath())); 275 | 276 | if (includePathExceptionsInClasspath) { 277 | resolvePathsResult.getPathExceptions().keySet() 278 | .forEach(file -> classpathElements.add(file.getPath())); 279 | } 280 | } else { 281 | // non-modular projects 282 | pathElements.forEach((k, v) -> { 283 | if (v != null && v.name() != null && v.name().startsWith(JAVAFX_PREFIX)) { 284 | // only JavaFX jars are required in the module-path 285 | modulepathElements.add(k); 286 | } else { 287 | classpathElements.add(k); 288 | } 289 | }); 290 | } 291 | } catch (Exception e) { 292 | getLog().warn(e.getMessage()); 293 | } 294 | 295 | if (runtimePathOption == MODULEPATH) { 296 | getLog().debug(runtimePathOption + " runtimePathOption set by user. Moving all jars to modulepath."); 297 | modulepathElements.addAll(classpathElements); 298 | classpathElements.clear(); 299 | } else if (runtimePathOption == CLASSPATH) { 300 | getLog().debug(runtimePathOption + " runtimePathOption set by user. Moving all jars to classpath."); 301 | classpathElements.addAll(modulepathElements); 302 | modulepathElements.clear(); 303 | if (mainClass.contains("/")) { 304 | getLog().warn("Module name found in with runtimePathOption set as CLASSPATH. Module name will be ignored."); 305 | } 306 | if (doesExtendFXApplication(createMainClassString(mainClass, moduleDescriptor, runtimePathOption))) { 307 | throw new MojoExecutionException("Launcher class is required. Main-class cannot extend Application when running JavaFX application on CLASSPATH"); 308 | } 309 | } 310 | 311 | getLog().debug("Classpath:" + classpathElements.size()); 312 | classpathElements.forEach(s -> getLog().debug(" " + s)); 313 | 314 | getLog().debug("Modulepath: " + modulepathElements.size()); 315 | modulepathElements.forEach(s -> getLog().debug(" " + s)); 316 | 317 | getLog().debug("pathElements: " + pathElements.size()); 318 | pathElements.forEach((k, v) -> getLog().debug(" " + k + " :: " + (v != null && v.name() != null ? v.name() : v))); 319 | } 320 | 321 | private JavaModuleDescriptor createModuleDescriptor(ResolvePathsResult resolvePathsResult) throws MojoExecutionException { 322 | if (runtimePathOption == CLASSPATH) { 323 | getLog().info(CLASSPATH + " runtimePathOption set by user. module-info.java will be ignored."); 324 | return null; 325 | } 326 | return resolvePathsResult.getMainModuleDescriptor(); 327 | } 328 | 329 | private List getCompileClasspathElements(MavenProject project) { 330 | List list = new ArrayList<>(); 331 | list.add(new File(project.getBuild().getOutputDirectory())); 332 | 333 | // include systemPath dependencies 334 | list.addAll(project.getDependencies().stream() 335 | .filter(d -> d.getSystemPath() != null && ! d.getSystemPath().isEmpty()) 336 | .map(d -> new File(d.getSystemPath())) 337 | .collect(Collectors.toList())); 338 | 339 | list.addAll(project.getArtifacts().stream() 340 | .sorted((a1, a2) -> { 341 | int compare = a1.compareTo(a2); 342 | if (compare == 0) { 343 | // give precedence to classifiers 344 | return a1.hasClassifier() ? 1 : (a2.hasClassifier() ? -1 : 0); 345 | } 346 | return compare; 347 | }) 348 | .map(Artifact::getFile) 349 | .collect(Collectors.toList())); 350 | return list.stream() 351 | .distinct() 352 | .collect(Collectors.toList()); 353 | } 354 | 355 | void handleWorkingDirectory() throws MojoExecutionException { 356 | if (workingDirectory == null) { 357 | workingDirectory = basedir; 358 | } 359 | 360 | if (!workingDirectory.exists()) { 361 | getLog().debug("Making working directory '" + workingDirectory.getAbsolutePath() + "'."); 362 | if (!workingDirectory.mkdirs()) { 363 | throw new MojoExecutionException("Could not make working directory: '" + workingDirectory.getAbsolutePath() + "'"); 364 | } 365 | } 366 | } 367 | 368 | Map handleSystemEnvVariables() { 369 | Map enviro = new HashMap<>(); 370 | try { 371 | Properties systemEnvVars = CommandLineUtils.getSystemEnvVars(); 372 | for (Map.Entry entry : systemEnvVars.entrySet()) { 373 | enviro.put((String) entry.getKey(), (String) entry.getValue()); 374 | } 375 | } catch (IOException x) { 376 | getLog().error("Could not assign default system environment variables.", x); 377 | } 378 | 379 | return enviro; 380 | } 381 | 382 | CommandLine getExecutablePath(String executable, Map enviro, File dir) { 383 | File execFile = new File(executable); 384 | String exec = null; 385 | if (execFile.isFile()) { 386 | getLog().debug("'executable' parameter is set to " + executable); 387 | exec = execFile.getAbsolutePath(); 388 | } 389 | 390 | if (exec == null && toolchainManager != null) { 391 | Toolchain toolchain = toolchainManager.getToolchainFromBuildContext("jdk", session); 392 | if (toolchain != null) { 393 | getLog().info("Toolchain in javafx-maven-plugin " + toolchain); 394 | exec = toolchain.findTool("java"); 395 | getLog().debug("Tool in toolchain in javafx-maven-plugin " + exec); 396 | } 397 | } 398 | 399 | if (exec == null) { 400 | String javaHomeFromEnv = getJavaHomeEnv(enviro); 401 | if (javaHomeFromEnv != null && ! javaHomeFromEnv.isEmpty()) { 402 | exec = findExecutable(executable, Arrays.asList(javaHomeFromEnv.concat(File.separator).concat("bin"))); 403 | } 404 | } 405 | 406 | if (exec == null && OS.isFamilyWindows()) { 407 | List paths = this.getExecutablePaths(enviro); 408 | paths.add(0, dir.getAbsolutePath()); 409 | exec = findExecutable(executable, paths); 410 | } 411 | 412 | if (exec == null) { 413 | exec = executable; 414 | } 415 | 416 | CommandLine toRet; 417 | if (OS.isFamilyWindows() && !hasNativeExtension(exec) && hasExecutableExtension(exec) ) { 418 | // run the windows batch script in isolation and exit at the end 419 | final String comSpec = System.getenv( "ComSpec" ); 420 | toRet = new CommandLine( comSpec == null ? "cmd" : comSpec ); 421 | toRet.addArgument( "/c" ); 422 | toRet.addArgument( exec ); 423 | } else { 424 | toRet = new CommandLine(exec); 425 | } 426 | getLog().debug("Executable " + toRet.toString()); 427 | return toRet; 428 | } 429 | 430 | int executeCommandLine(Executor exec, CommandLine commandLine, Map enviro, 431 | OutputStream out, OutputStream err) throws ExecuteException, IOException { 432 | // note: don't use BufferedOutputStream here since it delays the outputs MEXEC-138 433 | PumpStreamHandler psh = new PumpStreamHandler(out, err, System.in); 434 | return executeCommandLine(exec, commandLine, enviro, psh); 435 | } 436 | 437 | int executeCommandLine(Executor exec, CommandLine commandLine, Map enviro, 438 | FileOutputStream outputFile) throws ExecuteException, IOException { 439 | BufferedOutputStream bos = new BufferedOutputStream(outputFile); 440 | PumpStreamHandler psh = new PumpStreamHandler(bos); 441 | return executeCommandLine(exec, commandLine, enviro, psh); 442 | } 443 | 444 | /** 445 | * Returns the path of the parent directory. 446 | * At the given depth if the path has no parent, the method returns null. 447 | * @param path Path against which the parent needs to be evaluated 448 | * @param depth Depth of the path relative to parent 449 | * @return Path to the parent, if exists. Null, otherwise. 450 | */ 451 | static Path getParent(Path path, int depth) { 452 | if (path == null || !Files.exists(path) || depth > path.getNameCount()) { 453 | return null; 454 | } 455 | return path.getRoot().resolve(path.subpath(0, path.getNameCount() - depth)); 456 | } 457 | 458 | String createMainClassString(String mainClass, JavaModuleDescriptor moduleDescriptor, RuntimePathOption runtimePathOption) { 459 | Objects.requireNonNull(mainClass, "Main class cannot be null"); 460 | if (runtimePathOption == CLASSPATH) { 461 | if (mainClass.contains("/")) { 462 | return mainClass.substring(mainClass.indexOf("/") + 1); 463 | } 464 | return mainClass; 465 | } 466 | if (moduleDescriptor != null && !mainClass.contains("/")) { 467 | getLog().warn("Module name not found in . Module name will be assumed from module-info.java"); 468 | return moduleDescriptor.name() + "/" + mainClass; 469 | } 470 | return mainClass; 471 | } 472 | 473 | private static String findExecutable(final String executable, final List paths) { 474 | File f = null; 475 | search: for (final String path : paths) { 476 | f = new File(path, executable); 477 | if (!OS.isFamilyWindows() && f.isFile()) { 478 | break; 479 | } else { 480 | for (final String extension : getExecutableExtensions()) { 481 | f = new File(path, executable + extension); 482 | if (f.isFile()) { 483 | break search; 484 | } 485 | } 486 | } 487 | } 488 | 489 | if (f == null || !f.exists()) { 490 | return null; 491 | } 492 | return f.getAbsolutePath(); 493 | } 494 | 495 | private static boolean hasNativeExtension(final String exec) { 496 | final String lowerCase = exec.toLowerCase(); 497 | return lowerCase.endsWith(".exe") || lowerCase.endsWith(".com"); 498 | } 499 | 500 | private static boolean hasExecutableExtension(final String exec) { 501 | final String lowerCase = exec.toLowerCase(); 502 | for (final String ext : getExecutableExtensions()) { 503 | if (lowerCase.endsWith(ext)) { 504 | return true; 505 | } 506 | } 507 | return false; 508 | } 509 | 510 | private static List getExecutableExtensions() { 511 | final String pathExt = System.getenv("PATHEXT"); 512 | return pathExt == null ? Arrays.asList(".bat", ".cmd") 513 | : Arrays.asList(StringUtils.split(pathExt.toLowerCase(), File.pathSeparator)); 514 | } 515 | 516 | private List getExecutablePaths(Map enviro) { 517 | List paths = new ArrayList<>(); 518 | paths.add(""); 519 | 520 | String path = enviro.get("PATH"); 521 | if (path != null) { 522 | paths.addAll(Arrays.asList(StringUtils.split(path, File.pathSeparator))); 523 | } 524 | return paths; 525 | } 526 | 527 | private String getJavaHomeEnv(Map enviro) { 528 | String javahome = enviro.get("JAVA_HOME"); 529 | if (javahome == null || javahome.isEmpty()) { 530 | return null; 531 | } 532 | 533 | int pathStartIndex = javahome.charAt(0) == '"' ? 1 : 0; 534 | int pathEndIndex = javahome.charAt(javahome.length() - 1) == '"' ? javahome.length() - 1 : javahome.length(); 535 | 536 | return javahome.substring(pathStartIndex, pathEndIndex); 537 | } 538 | 539 | private int executeCommandLine(Executor exec, final CommandLine commandLine, Map enviro, 540 | final PumpStreamHandler psh) throws ExecuteException, IOException { 541 | exec.setStreamHandler(psh); 542 | 543 | int result; 544 | try { 545 | psh.start(); 546 | if (async) { 547 | if (asyncDestroyOnShutdown) { 548 | exec.setProcessDestroyer(getProcessDestroyer()); 549 | } 550 | 551 | exec.execute(commandLine, enviro, new ExecuteResultHandler() { 552 | public void onProcessFailed(ExecuteException e) { 553 | getLog().error("Async process failed for: " + commandLine, e); 554 | } 555 | 556 | public void onProcessComplete(int exitValue) { 557 | getLog().debug("Async process complete, exit value = " + exitValue + " for: " + commandLine); 558 | try { 559 | psh.stop(); 560 | } catch (IOException e) { 561 | getLog().error("Error stopping async process stream handler for: " + commandLine, e); 562 | } 563 | } 564 | }); 565 | result = 0; 566 | } else { 567 | result = exec.execute(commandLine, enviro); 568 | } 569 | } finally { 570 | if (!async) { 571 | psh.stop(); 572 | } 573 | } 574 | return result; 575 | } 576 | 577 | private ProcessDestroyer getProcessDestroyer() { 578 | if (processDestroyer == null) { 579 | processDestroyer = new ShutdownHookProcessDestroyer(); 580 | } 581 | return processDestroyer; 582 | } 583 | 584 | private boolean doesExtendFXApplication(String mainClass) { 585 | boolean fxApplication = false; 586 | try { 587 | List pathUrls = new ArrayList<>(); 588 | for (String path : project.getCompileClasspathElements()) { 589 | pathUrls.add(new File(path).toURI().toURL()); 590 | } 591 | URL[] urls = pathUrls.toArray(new URL[0]); 592 | URLClassLoader classLoader = new URLClassLoader(urls, JavaFXBaseMojo.class.getClassLoader()); 593 | Class clazz = Class.forName(mainClass, false, classLoader); 594 | fxApplication = doesExtendFXApplication(clazz); 595 | getLog().debug("Main Class " + clazz.toString() + " extends Application: " + fxApplication); 596 | } catch (NoClassDefFoundError | ClassNotFoundException | DependencyResolutionRequiredException | MalformedURLException e) { 597 | getLog().debug(e); 598 | } 599 | return fxApplication; 600 | } 601 | 602 | private static boolean doesExtendFXApplication(Class mainClass) { 603 | for (Class sc = mainClass.getSuperclass(); sc != null; 604 | sc = sc.getSuperclass()) { 605 | if (sc.getName().equals(JAVAFX_APPLICATION_CLASS_NAME)) { 606 | return true; 607 | } 608 | } 609 | return false; 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /src/main/java/org/openjfx/JavaFXJLinkMojo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | import org.apache.commons.exec.CommandLine; 20 | import org.apache.commons.exec.DefaultExecutor; 21 | import org.apache.commons.exec.ExecuteException; 22 | import org.apache.commons.exec.Executor; 23 | import org.apache.commons.exec.OS; 24 | import org.apache.maven.plugin.MojoExecutionException; 25 | import org.apache.maven.plugin.MojoFailureException; 26 | import org.apache.maven.plugins.annotations.Component; 27 | import org.apache.maven.plugins.annotations.Execute; 28 | import org.apache.maven.plugins.annotations.LifecyclePhase; 29 | import org.apache.maven.plugins.annotations.Mojo; 30 | import org.apache.maven.plugins.annotations.Parameter; 31 | import org.apache.maven.plugins.annotations.ResolutionScope; 32 | import org.codehaus.plexus.archiver.Archiver; 33 | import org.codehaus.plexus.archiver.ArchiverException; 34 | import org.codehaus.plexus.archiver.zip.ZipArchiver; 35 | import org.codehaus.plexus.util.IOUtil; 36 | import org.codehaus.plexus.util.StringUtils; 37 | 38 | import java.io.ByteArrayOutputStream; 39 | import java.io.File; 40 | import java.io.FileOutputStream; 41 | import java.io.IOException; 42 | import java.nio.file.Files; 43 | import java.nio.file.Path; 44 | import java.nio.file.Paths; 45 | import java.util.ArrayList; 46 | import java.util.Comparator; 47 | import java.util.List; 48 | import java.util.Map; 49 | import java.util.Objects; 50 | import java.util.regex.Pattern; 51 | import java.util.stream.Collectors; 52 | 53 | @Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME) 54 | @Execute(phase = LifecyclePhase.PROCESS_CLASSES) 55 | public class JavaFXJLinkMojo extends JavaFXBaseMojo { 56 | 57 | private static final Pattern JLINK_VERSION_PATTERN = Pattern.compile("(1[3-9]|[2-9][0-9]|\\d{3,})"); 58 | 59 | /** 60 | * Strips debug information out, equivalent to -G, --strip-debug, 61 | * default false 62 | */ 63 | @Parameter(property = "javafx.stripDebug", defaultValue = "false") 64 | private boolean stripDebug; 65 | 66 | /** 67 | * Strip Java debug attributes out, equivalent to --strip-java-debug-attributes, 68 | * default false 69 | */ 70 | @Parameter(property = "javafx.stripJavaDebugAttributes", defaultValue = "false") 71 | private boolean stripJavaDebugAttributes; 72 | 73 | /** 74 | * Compression level of the resources being used, equivalent to: 75 | * -c, --compress=level. Valid values: 0, 1, 2, 76 | * default 0 77 | */ 78 | @Parameter(property = "javafx.compress", defaultValue = "0") 79 | private Integer compress; 80 | 81 | /** 82 | * Remove the includes directory in the resulting runtime image, 83 | * equivalent to: --no-header-files, default false 84 | */ 85 | @Parameter(property = "javafx.noHeaderFiles", defaultValue = "false") 86 | private boolean noHeaderFiles; 87 | 88 | /** 89 | * Remove the man directory in the resulting Java runtime image, 90 | * equivalent to: --no-man-pages, default false 91 | */ 92 | @Parameter(property = "javafx.noManPages", defaultValue = "false") 93 | private boolean noManPages; 94 | 95 | /** 96 | * Add the option --bind-services or not, default false. 97 | */ 98 | @Parameter(property = "javafx.bindServices", defaultValue = "false") 99 | private boolean bindServices; 100 | 101 | /** 102 | * --ignore-signing-information, default false 103 | */ 104 | @Parameter(property = "javafx.ignoreSigningInformation", defaultValue = "false") 105 | private boolean ignoreSigningInformation; 106 | 107 | /** 108 | * Turn on verbose mode, equivalent to: --verbose, default false 109 | */ 110 | @Parameter(property = "javafx.jlinkVerbose", defaultValue = "false") 111 | private boolean jlinkVerbose; 112 | 113 | /** 114 | * Add a launcher script, equivalent to: 115 | * --launcher <name>=<module>[/<mainclass>]. 116 | */ 117 | @Parameter(property = "javafx.launcher") 118 | private String launcher; 119 | 120 | /** 121 | * The name of the folder with the resulting runtime image, 122 | * equivalent to --output <path> 123 | */ 124 | @Parameter(property = "javafx.jlinkImageName", defaultValue = "image") 125 | private String jlinkImageName; 126 | 127 | /** 128 | * When set, creates a zip of the resulting runtime image. 129 | */ 130 | @Parameter(property = "javafx.jlinkZipName") 131 | private String jlinkZipName; 132 | 133 | /** 134 | *

135 | * The executable. Can be a full path or the name of the executable. 136 | * In the latter case, the executable must be in the PATH for the execution to work. 137 | *

138 | */ 139 | @Parameter(property = "javafx.jlinkExecutable", defaultValue = "jlink") 140 | private String jlinkExecutable; 141 | 142 | /** 143 | * Optional jmodsPath path for local builds. 144 | */ 145 | @Parameter(property = "javafx.jmodsPath") 146 | private String jmodsPath; 147 | 148 | /** 149 | * The JAR archiver needed for archiving the environments. 150 | */ 151 | @Component(role = Archiver.class, hint = "zip") 152 | private ZipArchiver zipArchiver; 153 | 154 | public void execute() throws MojoExecutionException { 155 | if (skip) { 156 | getLog().info( "skipping execute as per configuration" ); 157 | return; 158 | } 159 | 160 | if (jlinkExecutable == null) { 161 | throw new MojoExecutionException("The parameter 'jlinkExecutable' is missing or invalid"); 162 | } 163 | 164 | if (basedir == null) { 165 | throw new IllegalStateException( "basedir is null. Should not be possible." ); 166 | } 167 | 168 | handleWorkingDirectory(); 169 | 170 | Map enviro = handleSystemEnvVariables(); 171 | CommandLine commandLine = getExecutablePath(jlinkExecutable, enviro, workingDirectory); 172 | 173 | if (isTargetUsingJava8(commandLine)) { 174 | getLog().info("Jlink not supported with Java 1.8"); 175 | return; 176 | } 177 | 178 | if (stripJavaDebugAttributes && !isJLinkVersion13orHigher(commandLine.getExecutable())) { 179 | stripJavaDebugAttributes = false; 180 | getLog().warn("JLink parameter --strip-java-debug-attributes only supported for version 13 and higher"); 181 | getLog().warn("The option 'stripJavaDebugAttributes' was skipped"); 182 | } 183 | 184 | try { 185 | 186 | List commandArguments = createCommandArguments(); 187 | String[] args = commandArguments.toArray(new String[commandArguments.size()]); 188 | commandLine.addArguments(args, false); 189 | getLog().debug("Executing command line: " + commandLine); 190 | 191 | Executor exec = new DefaultExecutor(); 192 | exec.setWorkingDirectory(workingDirectory); 193 | 194 | try { 195 | int resultCode; 196 | if (outputFile != null) { 197 | if ( !outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { 198 | getLog().warn( "Could not create non existing parent directories for log file: " + outputFile ); 199 | } 200 | 201 | FileOutputStream outputStream = null; 202 | try { 203 | outputStream = new FileOutputStream(outputFile); 204 | resultCode = executeCommandLine(exec, commandLine, enviro, outputStream); 205 | } finally { 206 | IOUtil.close(outputStream); 207 | } 208 | } else { 209 | resultCode = executeCommandLine(exec, commandLine, enviro, System.out, System.err); 210 | } 211 | 212 | if (resultCode != 0) { 213 | String message = "Result of " + commandLine.toString() + " execution is: '" + resultCode + "'."; 214 | getLog().error(message); 215 | throw new MojoExecutionException(message); 216 | } 217 | 218 | if (launcher != null && ! launcher.isEmpty()) { 219 | patchLauncherScript(launcher); 220 | 221 | if (OS.isFamilyWindows()) { 222 | patchLauncherScript(launcher + ".bat"); 223 | } 224 | } 225 | 226 | if (jlinkZipName != null && ! jlinkZipName.isEmpty()) { 227 | getLog().debug("Creating zip of runtime image"); 228 | File createZipArchiveFromImage = createZipArchiveFromImage(); 229 | project.getArtifact().setFile(createZipArchiveFromImage); 230 | } 231 | 232 | } catch (ExecuteException e) { 233 | getLog().error("Command execution failed.", e); 234 | e.printStackTrace(); 235 | throw new MojoExecutionException("Command execution failed.", e); 236 | } catch (IOException e) { 237 | getLog().error("Command execution failed.", e); 238 | throw new MojoExecutionException("Command execution failed.", e); 239 | } 240 | } catch (Exception e) { 241 | throw new MojoExecutionException("Error", e); 242 | } 243 | } 244 | 245 | private void patchLauncherScript(String launcherFilename) throws IOException { 246 | Path launcherPath = Paths.get(builddir.getAbsolutePath(), jlinkImageName, "bin", launcherFilename); 247 | 248 | if (!Files.exists(launcherPath)) { 249 | getLog().debug("Launcher file not exist: " + launcherPath); 250 | return; 251 | } 252 | 253 | if (options != null) { 254 | String optionsString = options.stream() 255 | .filter(Objects::nonNull) 256 | .filter(String.class::isInstance) 257 | .map(String.class::cast) 258 | .collect(Collectors.joining(" ")); 259 | 260 | // Add vm options to launcher script 261 | List lines = Files.lines(launcherPath) 262 | .map(line -> { 263 | boolean unixOptionsLine = "JLINK_VM_OPTIONS=".equals(line); 264 | boolean winOptionsLine = "set JLINK_VM_OPTIONS=".equals(line); 265 | 266 | if (unixOptionsLine || winOptionsLine) { 267 | String lineWrapper = unixOptionsLine ? "\"" : ""; 268 | return line + lineWrapper + optionsString + lineWrapper; 269 | } 270 | return line; 271 | }) 272 | .collect(Collectors.toList()); 273 | Files.write(launcherPath, lines); 274 | } 275 | 276 | if (commandlineArgs != null) { 277 | // Add options to launcher script 278 | List lines = Files.lines(launcherPath) 279 | .map(line -> { 280 | if (line.endsWith("$@")) { 281 | return line.replace("$@", commandlineArgs + " $@"); 282 | } 283 | return line; 284 | }) 285 | .collect(Collectors.toList()); 286 | Files.write(launcherPath, lines); 287 | } 288 | } 289 | 290 | private List createCommandArguments() throws MojoExecutionException, MojoFailureException { 291 | List commandArguments = new ArrayList<>(); 292 | preparePaths(getParent(Paths.get(jlinkExecutable), 2)); 293 | if (modulepathElements != null && !modulepathElements.isEmpty()) { 294 | commandArguments.add(" --module-path"); 295 | String modulePath = StringUtils.join(modulepathElements.iterator(), File.pathSeparator); 296 | if (jmodsPath != null && ! jmodsPath.isEmpty()) { 297 | getLog().debug("Including jmods from local path: " + jmodsPath); 298 | modulePath = jmodsPath + File.pathSeparator + modulePath; 299 | } 300 | commandArguments.add(modulePath); 301 | 302 | commandArguments.add(" --add-modules"); 303 | if (moduleDescriptor != null) { 304 | commandArguments.add(" " + moduleDescriptor.name()); 305 | } else { 306 | throw new MojoExecutionException("jlink requires a module descriptor"); 307 | } 308 | } 309 | 310 | commandArguments.add(" --output"); 311 | File image = new File(builddir, jlinkImageName); 312 | getLog().debug("image output: " + image.getAbsolutePath()); 313 | if (image.exists()) { 314 | try { 315 | Files.walk(image.toPath()) 316 | .sorted(Comparator.reverseOrder()) 317 | .map(Path::toFile) 318 | .forEach(File::delete); 319 | } catch (IOException e) { 320 | throw new MojoExecutionException("Image can't be removed " + image.getAbsolutePath(), e); 321 | } 322 | } 323 | commandArguments.add(" " + image.getAbsolutePath()); 324 | 325 | if (stripDebug) { 326 | commandArguments.add(" --strip-debug"); 327 | } 328 | if (stripJavaDebugAttributes) { 329 | commandArguments.add(" --strip-java-debug-attributes"); 330 | } 331 | if (bindServices) { 332 | commandArguments.add(" --bind-services"); 333 | } 334 | if (ignoreSigningInformation) { 335 | commandArguments.add(" --ignore-signing-information"); 336 | } 337 | if (compress != null) { 338 | commandArguments.add(" --compress"); 339 | if (compress < 0 || compress > 2) { 340 | throw new MojoFailureException("The given compress parameters " + compress + " is not in the valid value range from 0..2"); 341 | } 342 | commandArguments.add(" " + compress); 343 | } 344 | if (noHeaderFiles) { 345 | commandArguments.add(" --no-header-files"); 346 | } 347 | if (noManPages) { 348 | commandArguments.add(" --no-man-pages"); 349 | } 350 | if (jlinkVerbose) { 351 | commandArguments.add(" --verbose"); 352 | } 353 | 354 | if (launcher != null && ! launcher.isEmpty()) { 355 | commandArguments.add(" --launcher"); 356 | String moduleMainClass; 357 | if (mainClass.contains("/")) { 358 | moduleMainClass = mainClass; 359 | } else { 360 | moduleMainClass = moduleDescriptor.name() + "/" + mainClass; 361 | } 362 | commandArguments.add(" " + launcher + "=" + moduleMainClass); 363 | } 364 | return commandArguments; 365 | } 366 | 367 | private File createZipArchiveFromImage() throws MojoExecutionException { 368 | File imageArchive = new File(builddir, jlinkImageName); 369 | zipArchiver.addDirectory(imageArchive); 370 | 371 | File resultArchive = new File(builddir, jlinkZipName + ".zip"); 372 | zipArchiver.setDestFile(resultArchive); 373 | try { 374 | zipArchiver.createArchive(); 375 | } catch (ArchiverException | IOException e) { 376 | throw new MojoExecutionException(e.getMessage(), e); 377 | } 378 | return resultArchive; 379 | } 380 | 381 | private boolean isJLinkVersion13orHigher(String jlinkExePath) { 382 | CommandLine versionCommandLine = new CommandLine(jlinkExePath) 383 | .addArgument("--version"); 384 | 385 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 386 | int resultCode = -1; 387 | 388 | try { 389 | resultCode = executeCommandLine(new DefaultExecutor(), versionCommandLine, null, baos, System.err); 390 | } catch (IOException e) { 391 | if (getLog().isDebugEnabled()) { 392 | getLog().error("Error getting JLink version", e); 393 | } 394 | } 395 | 396 | if (resultCode != 0) { 397 | getLog().error("Unable to get JLink version"); 398 | getLog().error("Result of " + versionCommandLine + " execution is: '" + resultCode + "'"); 399 | return false; 400 | } 401 | 402 | String versionStr = new String(baos.toByteArray()); 403 | return JLINK_VERSION_PATTERN.matcher(versionStr).lookingAt(); 404 | } 405 | 406 | // for tests 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/org/openjfx/JavaFXRunMojo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, 2020, Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openjfx; 17 | 18 | import org.apache.commons.exec.CommandLine; 19 | import org.apache.commons.exec.DefaultExecutor; 20 | import org.apache.commons.exec.ExecuteException; 21 | import org.apache.commons.exec.Executor; 22 | import org.apache.maven.plugin.MojoExecutionException; 23 | import org.apache.maven.plugins.annotations.Execute; 24 | import org.apache.maven.plugins.annotations.LifecyclePhase; 25 | import org.apache.maven.plugins.annotations.Mojo; 26 | import org.apache.maven.plugins.annotations.Parameter; 27 | import org.apache.maven.plugins.annotations.ResolutionScope; 28 | import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor; 29 | import org.codehaus.plexus.util.IOUtil; 30 | import org.codehaus.plexus.util.StringUtils; 31 | 32 | import java.io.File; 33 | import java.io.FileOutputStream; 34 | import java.io.IOException; 35 | import java.nio.file.Paths; 36 | import java.util.ArrayList; 37 | import java.util.Collection; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.Objects; 41 | import java.util.regex.Pattern; 42 | import java.util.stream.Collectors; 43 | 44 | import static org.openjfx.model.RuntimePathOption.CLASSPATH; 45 | import static org.openjfx.model.RuntimePathOption.MODULEPATH; 46 | 47 | @Mojo(name = "run", requiresDependencyResolution = ResolutionScope.RUNTIME) 48 | @Execute(phase = LifecyclePhase.PROCESS_CLASSES) 49 | public class JavaFXRunMojo extends JavaFXBaseMojo { 50 | 51 | /** 52 | *

53 | * The executable. Can be a full path or the name of the executable. In the latter case, the executable must be in 54 | * the PATH for the execution to work. 55 | *

56 | */ 57 | @Parameter(property = "javafx.executable", defaultValue = "java") 58 | private String executable; 59 | 60 | public void execute() throws MojoExecutionException { 61 | if (skip) { 62 | getLog().info( "skipping execute as per configuration" ); 63 | return; 64 | } 65 | 66 | if (executable == null) { 67 | throw new MojoExecutionException("The parameter 'executable' is missing or invalid"); 68 | } 69 | 70 | if (basedir == null) { 71 | throw new IllegalStateException( "basedir is null. Should not be possible." ); 72 | } 73 | 74 | try { 75 | handleWorkingDirectory(); 76 | 77 | Map enviro = handleSystemEnvVariables(); 78 | CommandLine commandLine = getExecutablePath(executable, enviro, workingDirectory); 79 | 80 | boolean usingOldJDK = isTargetUsingJava8(commandLine); 81 | 82 | List commandArguments = createCommandArguments(usingOldJDK); 83 | String[] args = commandArguments.toArray(new String[commandArguments.size()]); 84 | commandLine.addArguments(args, false); 85 | getLog().debug("Executing command line: " + commandLine); 86 | 87 | Executor exec = new DefaultExecutor(); 88 | exec.setWorkingDirectory(workingDirectory); 89 | 90 | try { 91 | int resultCode; 92 | if (outputFile != null) { 93 | if ( !outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { 94 | getLog().warn( "Could not create non existing parent directories for log file: " + outputFile ); 95 | } 96 | 97 | FileOutputStream outputStream = null; 98 | try { 99 | outputStream = new FileOutputStream(outputFile); 100 | resultCode = executeCommandLine(exec, commandLine, enviro, outputStream); 101 | } finally { 102 | IOUtil.close(outputStream); 103 | } 104 | } else { 105 | resultCode = executeCommandLine(exec, commandLine, enviro, System.out, System.err); 106 | } 107 | 108 | if (resultCode != 0) { 109 | String message = "Result of " + commandLine.toString() + " execution is: '" + resultCode + "'."; 110 | getLog().error(message); 111 | throw new MojoExecutionException(message); 112 | } 113 | } catch (ExecuteException e) { 114 | getLog().error("Command execution failed.", e); 115 | e.printStackTrace(); 116 | throw new MojoExecutionException("Command execution failed.", e); 117 | } catch (IOException e) { 118 | getLog().error("Command execution failed.", e); 119 | throw new MojoExecutionException("Command execution failed.", e); 120 | } 121 | } catch (Exception e) { 122 | throw new MojoExecutionException("Error", e); 123 | } 124 | } 125 | 126 | private List createCommandArguments(boolean oldJDK) throws MojoExecutionException { 127 | List commandArguments = new ArrayList<>(); 128 | preparePaths(getParent(Paths.get(executable), 2)); 129 | 130 | if (options != null) { 131 | options.stream() 132 | .filter(Objects::nonNull) 133 | .filter(String.class::isInstance) 134 | .map(String.class::cast) 135 | .map(this::splitComplexArgumentString) 136 | .flatMap(Collection::stream) 137 | .forEach(commandArguments::add); 138 | } 139 | if (!oldJDK) { 140 | if (runtimePathOption == MODULEPATH || modulepathElements != null && !modulepathElements.isEmpty()) { 141 | commandArguments.add("--module-path"); 142 | commandArguments.add(StringUtils.join(modulepathElements.iterator(), File.pathSeparator)); 143 | commandArguments.add("--add-modules"); 144 | commandArguments.add(createAddModulesString(moduleDescriptor, pathElements)); 145 | } 146 | } 147 | 148 | if (classpathElements != null && (oldJDK || !classpathElements.isEmpty())) { 149 | commandArguments.add("-classpath"); 150 | String classpath = ""; 151 | if (oldJDK || runtimePathOption == CLASSPATH) { 152 | classpath = project.getBuild().getOutputDirectory() + File.pathSeparator; 153 | } 154 | classpath += StringUtils.join(classpathElements.iterator(), File.pathSeparator); 155 | commandArguments.add(classpath); 156 | } 157 | 158 | if (mainClass != null) { 159 | if (moduleDescriptor != null) { 160 | commandArguments.add("--module"); 161 | } 162 | commandArguments.add(createMainClassString(mainClass, moduleDescriptor, runtimePathOption)); 163 | } 164 | 165 | if (commandlineArgs != null) { 166 | splitComplexArgumentString(commandlineArgs) 167 | .forEach(commandArguments::add); 168 | } 169 | return commandArguments; 170 | } 171 | 172 | private String createAddModulesString(JavaModuleDescriptor moduleDescriptor, Map pathElements) { 173 | if (moduleDescriptor == null) { 174 | return pathElements.values().stream() 175 | .filter(Objects::nonNull) 176 | .map(JavaModuleDescriptor::name) 177 | .filter(Objects::nonNull) 178 | .filter(module -> module.startsWith(JAVAFX_PREFIX) && !module.endsWith("Empty")) 179 | .collect(Collectors.joining(",")); 180 | } 181 | return moduleDescriptor.name(); 182 | } 183 | 184 | private List splitComplexArgumentString(String argumentString) { 185 | char[] strArr = argumentString.trim().toCharArray(); 186 | 187 | List splitedArgs = new ArrayList<>(); 188 | StringBuilder sb = new StringBuilder(); 189 | 190 | char expectedSeparator = ' '; 191 | for (int i = 0; i < strArr.length; i++) { 192 | char item = strArr[i]; 193 | 194 | if (item == expectedSeparator 195 | || (expectedSeparator == ' ' && Pattern.matches("\\s", String.valueOf(item))) ) { 196 | 197 | if (expectedSeparator == '"' || expectedSeparator == '\'') { 198 | sb.append(item); 199 | expectedSeparator = ' '; 200 | } else if (expectedSeparator == ' ' && sb.length() > 0) { 201 | splitedArgs.add(sb.toString()); 202 | sb.delete(0, sb.length()); 203 | } 204 | } else { 205 | if (expectedSeparator == ' ' && (item == '"' || item == '\'')) { 206 | expectedSeparator = item; 207 | } 208 | 209 | sb.append(item); 210 | } 211 | 212 | if (i == strArr.length - 1 && sb.length() > 0) { 213 | splitedArgs.add(sb.toString()); 214 | } 215 | } 216 | 217 | return splitedArgs; 218 | } 219 | 220 | // for tests 221 | 222 | void setExecutable(String executable) { 223 | this.executable = executable; 224 | } 225 | 226 | void setBasedir(File basedir) { 227 | this.basedir = basedir; 228 | } 229 | 230 | void setCommandlineArgs(String commandlineArgs) { 231 | this.commandlineArgs = commandlineArgs; 232 | } 233 | 234 | List splitComplexArgumentStringAdapter(String cliOptions) { 235 | return splitComplexArgumentString(cliOptions); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/org/openjfx/model/RuntimePathOption.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020, Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openjfx.model; 17 | 18 | /** 19 | * All the classes and dependencies are added to either the classpath or modulepath depending on the option set in the plugin configuration. 20 | * If not set, the plugin chooses the suitable option for classes and dependencies based on a few parameters, like presence of module descriptor file. 21 | */ 22 | public enum RuntimePathOption { 23 | /** 24 | * Puts all the dependencies on the classpath. If a module-info.java is present, it is ignored. 25 | * A Launcher class is 26 | * required to run a JavaFX application from the classpath. 27 | */ 28 | CLASSPATH, 29 | /** 30 | * Puts all the dependencies on the modulepath. 31 | */ 32 | MODULEPATH 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plexus/components.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | org.apache.maven.lifecycle.Lifecycle 6 | org.apache.maven.lifecycle.Lifecycle 7 | run 8 | 9 | run 10 | 11 | run 12 | 13 | 14 | 15 | org.openjfx:javafx-maven-plugin:run 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/java/org/openjfx/JavaFXBaseMojoTest.java: -------------------------------------------------------------------------------- 1 | package org.openjfx; 2 | 3 | import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor; 4 | import org.junit.AfterClass; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.Comparator; 16 | 17 | import static org.openjfx.model.RuntimePathOption.CLASSPATH; 18 | import static org.openjfx.model.RuntimePathOption.MODULEPATH; 19 | 20 | public class JavaFXBaseMojoTest { 21 | 22 | private static Path path; 23 | private static String tempDirPath; 24 | 25 | private JavaFXBaseMojo mojo; 26 | private JavaModuleDescriptor moduleDescriptor; 27 | 28 | @BeforeClass 29 | public static void setup() throws IOException { 30 | tempDirPath = System.getProperty("java.io.tmpdir"); 31 | path = Files.createDirectories(Paths.get(tempDirPath, "test", "test")); 32 | } 33 | 34 | @Before 35 | public void create() { 36 | mojo = new JavaFXBaseMojo() { 37 | @Override 38 | public void execute() { 39 | //no-op 40 | } 41 | }; 42 | moduleDescriptor = JavaModuleDescriptor.newModule("hellofx").build(); 43 | } 44 | 45 | @Test 46 | public void parentTest() { 47 | Assert.assertEquals(Paths.get(tempDirPath), JavaFXBaseMojo.getParent(path, 2)); 48 | } 49 | 50 | @Test 51 | public void mainClassStringWithModuleDescriptor() { 52 | Assert.assertEquals("hellofx/org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", moduleDescriptor, null)); 53 | } 54 | 55 | @Test 56 | public void mainClassStringWithoutModuleDescriptor() { 57 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", null, null)); 58 | Assert.assertEquals("hellofx/org.openjfx.Main", mojo.createMainClassString("hellofx/org.openjfx.Main", null, null)); 59 | } 60 | 61 | @Test 62 | public void mainClassStringWithClasspathWithModuleDescriptor() { 63 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", moduleDescriptor, CLASSPATH)); 64 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("hellofx/org.openjfx.Main", moduleDescriptor, CLASSPATH)); 65 | } 66 | 67 | @Test 68 | public void mainClassStringWithClasspathWithoutModuleDescriptor() { 69 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", null, CLASSPATH)); 70 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("hellofx/org.openjfx.Main", null, CLASSPATH)); 71 | } 72 | 73 | @Test 74 | public void mainClassStringWithModulepathWithModuleDescriptor() { 75 | Assert.assertEquals("hellofx/org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", moduleDescriptor, MODULEPATH)); 76 | Assert.assertEquals("hellofx/org.openjfx.Main", mojo.createMainClassString("hellofx/org.openjfx.Main", moduleDescriptor, MODULEPATH)); 77 | } 78 | 79 | @Test 80 | public void mainClassStringWithModulepathWithoutModuleDescriptor() { 81 | Assert.assertEquals("org.openjfx.Main", mojo.createMainClassString("org.openjfx.Main", null, MODULEPATH)); 82 | Assert.assertEquals("hellofx/org.openjfx.Main", mojo.createMainClassString("hellofx/org.openjfx.Main", null, MODULEPATH)); 83 | } 84 | 85 | @Test 86 | public void invalidParentTest() { 87 | Assert.assertNull(JavaFXBaseMojo.getParent(path, 10)); 88 | } 89 | 90 | @Test 91 | public void invalidPathTest() { 92 | Assert.assertNull(JavaFXBaseMojo.getParent(Paths.get("/some-invalid-path"), 0)); 93 | } 94 | 95 | @Test 96 | public void invalidPathWithDepthTest() { 97 | Assert.assertNull(JavaFXBaseMojo.getParent(Paths.get("/some-invalid-path"), 2)); 98 | } 99 | 100 | @AfterClass 101 | public static void destroy() throws IOException { 102 | Files.walk(path.getParent()) 103 | .sorted(Comparator.reverseOrder()) 104 | .map(Path::toFile) 105 | .filter(f -> "test".equals(f.getName())) 106 | .forEach(File::delete); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/org/openjfx/JavaFXRunMojoTestCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, 2020, Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | import org.apache.commons.exec.CommandLine; 20 | import org.apache.commons.exec.ExecuteException; 21 | import org.apache.commons.exec.Executor; 22 | import org.apache.commons.exec.OS; 23 | import org.apache.maven.artifact.Artifact; 24 | import org.apache.maven.execution.MavenSession; 25 | import org.apache.maven.monitor.logging.DefaultLog; 26 | import org.apache.maven.plugin.AbstractMojo; 27 | import org.apache.maven.plugin.MojoExecutionException; 28 | import org.apache.maven.plugin.MojoFailureException; 29 | import org.apache.maven.plugin.testing.AbstractMojoTestCase; 30 | import org.apache.maven.project.MavenProject; 31 | import org.apache.maven.project.ProjectBuilder; 32 | import org.apache.maven.project.ProjectBuildingRequest; 33 | import org.apache.maven.project.ProjectBuildingResult; 34 | import org.apache.maven.repository.RepositorySystem; 35 | import org.apache.maven.repository.internal.MavenRepositorySystemUtils; 36 | import org.codehaus.plexus.logging.Logger; 37 | import org.codehaus.plexus.logging.console.ConsoleLogger; 38 | import org.codehaus.plexus.util.StringOutputStream; 39 | import org.eclipse.aether.DefaultRepositorySystemSession; 40 | import org.eclipse.aether.artifact.DefaultArtifact; 41 | import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory; 42 | import org.eclipse.aether.repository.LocalRepository; 43 | import org.junit.Assert; 44 | import org.junit.Test; 45 | import org.mockito.Mock; 46 | import org.mockito.MockitoAnnotations; 47 | 48 | import java.io.File; 49 | import java.io.IOException; 50 | import java.io.OutputStream; 51 | import java.io.PrintStream; 52 | import java.util.*; 53 | import java.util.stream.Collectors; 54 | 55 | import static org.mockito.Mockito.mock; 56 | import static org.mockito.Mockito.when; 57 | 58 | public class JavaFXRunMojoTestCase extends AbstractMojoTestCase { 59 | 60 | private static final File LOCAL_REPO = new File( "src/test/repository" ); 61 | private static final String SOME_EXECUTABLE = UUID.randomUUID().toString(); 62 | 63 | @Mock 64 | private MavenSession session; 65 | 66 | private MockJavaFXRunMojo mojo; 67 | 68 | private class MockJavaFXRunMojo extends JavaFXRunMojo { 69 | 70 | public List commandLines = new ArrayList<>(); 71 | public String failureMsg; 72 | public int executeResult; 73 | 74 | @Override 75 | protected int executeCommandLine(Executor exec, CommandLine commandLine, Map enviro, OutputStream out, 76 | OutputStream err) throws IOException, ExecuteException { 77 | commandLines.add(commandLine); 78 | if (failureMsg != null) { 79 | throw new ExecuteException(failureMsg, executeResult); 80 | } 81 | return executeResult; 82 | } 83 | 84 | int getAmountExecutedCommandLines() { 85 | return commandLines.size(); 86 | } 87 | 88 | CommandLine getExecutedCommandline(int index) { 89 | return commandLines.get(index); 90 | } 91 | 92 | } 93 | 94 | public void setUp() throws Exception { 95 | super.setUp(); 96 | mojo = new MockJavaFXRunMojo(); 97 | mojo.executeResult = 0; 98 | mojo.setExecutable(SOME_EXECUTABLE); 99 | // mojo.setOptions(Arrays.asList("--version")); 100 | mojo.setCommandlineArgs("--version"); 101 | mojo.setBasedir(File.createTempFile("mvn-temp", "txt").getParentFile()); 102 | } 103 | 104 | public void testRunOK() throws MojoExecutionException { 105 | mojo.execute(); 106 | checkMojo(SOME_EXECUTABLE + " --version"); 107 | } 108 | 109 | public void testSimpleRun() throws Exception { 110 | final File testPom = getTestFile("src/test/resources/unit/javafxrun-basic-test/pom.xml"); 111 | JavaFXRunMojo mojo = getJavaFXRunMojo(testPom); 112 | String output = execute(mojo); 113 | assertEquals("JavaFXRun0", output.trim()); 114 | } 115 | 116 | public void testApplicationRun() throws Exception { 117 | final File testPom = getTestFile("src/test/resources/unit/javafxrun-app-test/pom.xml"); 118 | JavaFXRunMojo mojo = getJavaFXRunMojo(testPom); 119 | String output = execute(mojo); 120 | assertEquals("JavaFXRun1", output.trim()); 121 | } 122 | 123 | public void testClasspathLauncherRun() throws Exception { 124 | final File testPom = getTestFile("src/test/resources/unit/javafxrun-classpath-test/classpath-launcher.xml"); 125 | JavaFXRunMojo mojo = getJavaFXRunMojo(testPom); 126 | String output = execute(mojo); 127 | assertEquals("JavaFXRun1", output.trim()); 128 | } 129 | 130 | public void testClasspathNoLauncherRun() throws Exception { 131 | final File testPom = getTestFile("src/test/resources/unit/javafxrun-classpath-test/classpath-no-launcher.xml"); 132 | JavaFXRunMojo mojo = getJavaFXRunMojo(testPom); 133 | Exception e = null; 134 | try { 135 | execute(mojo); 136 | } catch (Exception e1) { 137 | e = e1; 138 | } 139 | assertEquals (true, (e instanceof MojoExecutionException)); 140 | } 141 | 142 | protected JavaFXRunMojo getJavaFXRunMojo(File testPom) throws Exception { 143 | JavaFXRunMojo mojo = (JavaFXRunMojo) lookupMojo("run", testPom); 144 | assertNotNull(mojo); 145 | 146 | setUpProject(testPom, mojo); 147 | MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" ); 148 | assertNotNull(project); 149 | 150 | if (! project.getDependencies().isEmpty()) { 151 | final MavenArtifactResolver resolver = new MavenArtifactResolver(project.getRepositories()); 152 | Set artifacts = project.getDependencies().stream() 153 | .map(d -> new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion())) 154 | .flatMap(a -> resolver.resolve(a).stream()) 155 | .collect(Collectors.toSet()); 156 | if (artifacts != null) { 157 | project.setArtifacts(artifacts); 158 | } 159 | } 160 | setVariableValueToObject(mojo, "compilePath", project.getCompileClasspathElements()); 161 | setVariableValueToObject(mojo, "session", session); 162 | setVariableValueToObject(mojo, "executable", "java"); 163 | setVariableValueToObject(mojo, "basedir", new File(getBasedir(), testPom.getParent())); 164 | 165 | return mojo; 166 | } 167 | 168 | private String execute(AbstractMojo mojo) throws MojoFailureException, MojoExecutionException, InterruptedException { 169 | PrintStream out = System.out; 170 | StringOutputStream stringOutputStream = new StringOutputStream(); 171 | System.setOut(new PrintStream(stringOutputStream)); 172 | mojo.setLog(new DefaultLog(new ConsoleLogger(Logger.LEVEL_ERROR, "javafx:run"))); 173 | 174 | try { 175 | mojo.execute(); 176 | } finally { 177 | Thread.sleep(300); 178 | System.setOut(out); 179 | } 180 | 181 | return stringOutputStream.toString(); 182 | } 183 | 184 | private void setUpProject(File pomFile, AbstractMojo mojo) throws Exception { 185 | super.setUp(); 186 | 187 | MockitoAnnotations.initMocks(this); 188 | 189 | ProjectBuildingRequest buildingRequest = mock(ProjectBuildingRequest.class); 190 | buildingRequest.setResolveDependencies(true); 191 | when(session.getProjectBuildingRequest()).thenReturn(buildingRequest); 192 | DefaultRepositorySystemSession repositorySession = MavenRepositorySystemUtils.newSession(); 193 | repositorySession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory() 194 | .newInstance(repositorySession, new LocalRepository(RepositorySystem.defaultUserLocalRepository))); 195 | when(buildingRequest.getRepositorySession()).thenReturn(repositorySession); 196 | 197 | ProjectBuilder builder = lookup(ProjectBuilder.class); 198 | ProjectBuildingResult build = builder.build(pomFile, buildingRequest); 199 | MavenProject project = build.getProject(); 200 | 201 | project.getBuild().setOutputDirectory(new File( "target/test-classes").getAbsolutePath()); 202 | setVariableValueToObject(mojo, "project", project); 203 | } 204 | 205 | private void checkMojo(String expectedCommandLine) { 206 | assertEquals(1, mojo.getAmountExecutedCommandLines()); 207 | CommandLine commandline = mojo.getExecutedCommandline(0); 208 | // do NOT depend on Commandline toString() 209 | assertEquals(expectedCommandLine, getCommandLineAsString(commandline)); 210 | } 211 | 212 | private String getCommandLineAsString(CommandLine commandline) { 213 | // for the sake of the test comparisons, cut out the eventual 214 | // cmd /c *.bat conversion 215 | String result = commandline.getExecutable(); 216 | boolean isCmd = false; 217 | if (OS.isFamilyWindows() && result.equals("cmd")) { 218 | result = ""; 219 | isCmd = true; 220 | } 221 | String[] arguments = commandline.getArguments(); 222 | for (int i = 0; i < arguments.length; i++) { 223 | String arg = arguments[i]; 224 | if (isCmd && i == 0 && "/c".equals(arg)) { 225 | continue; 226 | } 227 | if (isCmd && i == 1 && arg.endsWith(".bat")) { 228 | arg = arg.substring(0, arg.length() - ".bat".length()); 229 | } 230 | result += (result.length() == 0 ? "" : " ") + arg; 231 | } 232 | return result; 233 | } 234 | 235 | @Test 236 | public void testSplitComplexArgumentString() { 237 | String option = "param1 " + 238 | "param2 \n " + 239 | "param3\n" + 240 | "param4=\"/path/to/my file.log\" " + 241 | "'var\"foo var\"foo' " + 242 | "'var\"foo' " + 243 | "'var\"foo' " + 244 | "\"foo'var foo'var\" " + 245 | "\"foo'var\" " + 246 | "\"foo'var\""; 247 | 248 | String expected = "START," + 249 | "param1," + 250 | "param2," + 251 | "param3," + 252 | "param4=\"/path/to/my file.log\"," + 253 | "'var\"foo var\"foo'," + 254 | "'var\"foo'," + 255 | "'var\"foo'," + 256 | "\"foo'var foo'var\"," + 257 | "\"foo'var\"," + 258 | "\"foo'var\""; 259 | 260 | String splitedOption = new JavaFXRunMojo().splitComplexArgumentStringAdapter(option) 261 | .stream().reduce("START", (s1, s2) -> s1 + "," + s2); 262 | 263 | Assert.assertEquals(expected, splitedOption); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/test/java/org/openjfx/MavenArtifactResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | import org.apache.maven.artifact.handler.DefaultArtifactHandler; 20 | import org.apache.maven.model.Repository; 21 | import org.apache.maven.repository.internal.MavenRepositorySystemUtils; 22 | import org.eclipse.aether.DefaultRepositorySystemSession; 23 | import org.eclipse.aether.RepositorySystem; 24 | import org.eclipse.aether.RepositorySystemSession; 25 | import org.eclipse.aether.artifact.Artifact; 26 | import org.eclipse.aether.artifact.DefaultArtifact; 27 | import org.eclipse.aether.collection.CollectRequest; 28 | import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; 29 | import org.eclipse.aether.graph.Dependency; 30 | import org.eclipse.aether.graph.DependencyFilter; 31 | import org.eclipse.aether.impl.DefaultServiceLocator; 32 | import org.eclipse.aether.repository.LocalRepository; 33 | import org.eclipse.aether.repository.RemoteRepository; 34 | import org.eclipse.aether.resolution.ArtifactRequest; 35 | import org.eclipse.aether.resolution.ArtifactResolutionException; 36 | import org.eclipse.aether.resolution.ArtifactResult; 37 | import org.eclipse.aether.resolution.DependencyRequest; 38 | import org.eclipse.aether.resolution.DependencyResolutionException; 39 | import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; 40 | import org.eclipse.aether.spi.connector.transport.TransporterFactory; 41 | import org.eclipse.aether.transport.file.FileTransporterFactory; 42 | import org.eclipse.aether.transport.http.HttpTransporterFactory; 43 | import org.eclipse.aether.util.artifact.JavaScopes; 44 | import org.eclipse.aether.util.filter.DependencyFilterUtils; 45 | 46 | import java.util.Arrays; 47 | import java.util.LinkedList; 48 | import java.util.List; 49 | import java.util.Set; 50 | import java.util.stream.Collectors; 51 | 52 | /** 53 | * Resolve all transitive artifacts from a given dependency on a pom file 54 | * This is required because these are not resolved when running the tests 55 | */ 56 | class MavenArtifactResolver { 57 | 58 | private static String DEFAULT_LOCAL_REPO = org.apache.maven.repository.RepositorySystem. 59 | defaultUserLocalRepository.getAbsolutePath(); 60 | 61 | private final RepositorySystem repositorySystem; 62 | private final List remoteRepositories; 63 | 64 | MavenArtifactResolver(List repositories) { 65 | this.repositorySystem = createRepositorySystem(); 66 | this.remoteRepositories = new LinkedList<>(); 67 | repositories.forEach(r -> { 68 | RemoteRepository repository = new RemoteRepository 69 | .Builder(r.getId(), "default", r.getUrl()) 70 | .build(); 71 | remoteRepositories.add(repository); 72 | }); 73 | } 74 | 75 | private RepositorySystem createRepositorySystem() { 76 | DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); 77 | serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); 78 | serviceLocator.addService(TransporterFactory.class, FileTransporterFactory.class); 79 | serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); 80 | serviceLocator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() { 81 | @Override 82 | public void serviceCreationFailed(Class type, Class impl, Throwable exception) { 83 | throw new RuntimeException(exception); 84 | } 85 | }); 86 | return serviceLocator.getService(RepositorySystem.class); 87 | } 88 | 89 | private DefaultRepositorySystemSession createRepositorySystemSession(RepositorySystem system, String localRepoPath) { 90 | DefaultRepositorySystemSession systemSession = MavenRepositorySystemUtils.newSession(); 91 | LocalRepository localRepo = new LocalRepository(localRepoPath); 92 | systemSession.setLocalRepositoryManager(system.newLocalRepositoryManager(systemSession, localRepo)); 93 | return systemSession; 94 | } 95 | 96 | Set resolve(Artifact artifact) { 97 | RepositorySystemSession systemSession = createRepositorySystemSession(this.repositorySystem, DEFAULT_LOCAL_REPO); 98 | 99 | ArtifactResult resolvedArtifact; 100 | try { 101 | List artifactRequests = Arrays.asList( 102 | new ArtifactRequest(new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), 103 | artifact.getClassifier() != null ? artifact.getClassifier() : "", 104 | artifact.getExtension(), artifact.getVersion()), 105 | remoteRepositories, JavaScopes.RUNTIME)); 106 | List results = repositorySystem.resolveArtifacts(systemSession, artifactRequests); 107 | resolvedArtifact = results.get(results.size() - 1); 108 | } catch (ArtifactResolutionException e) { 109 | e.printStackTrace(); 110 | return null; 111 | } 112 | 113 | CollectRequest collectRequest = new CollectRequest(); 114 | collectRequest.setRoot(new Dependency(resolvedArtifact.getArtifact(), JavaScopes.COMPILE)); 115 | collectRequest.setRepositories(remoteRepositories); 116 | 117 | DependencyFilter classpathFilter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE); 118 | DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, classpathFilter); 119 | 120 | List artifactResults; 121 | try { 122 | artifactResults = repositorySystem.resolveDependencies(systemSession, dependencyRequest) 123 | .getArtifactResults(); 124 | } catch (DependencyResolutionException e) { 125 | e.printStackTrace(); 126 | return null; 127 | } 128 | 129 | return artifactResults.stream() 130 | .map(ArtifactResult::getArtifact) 131 | .map(a -> { 132 | org.apache.maven.artifact.Artifact ar = new org.apache.maven.artifact.DefaultArtifact( 133 | a.getGroupId(), a.getArtifactId(), a.getVersion(), 134 | "compile", "jar", a.getClassifier(), new DefaultArtifactHandler("jar")); 135 | ar.setFile(a.getFile()); 136 | return ar; 137 | }) 138 | .collect(Collectors.toSet()); 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/test/java/org/openjfx/TestJavaFXRun0.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | public class TestJavaFXRun0 { 20 | 21 | public TestJavaFXRun0() { 22 | System.out.println("JavaFXRun0"); 23 | } 24 | 25 | public static void main(String[] args) { 26 | new TestJavaFXRun0(); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/java/org/openjfx/TestJavaFXRun1.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openjfx; 18 | 19 | import javafx.application.Application; 20 | import javafx.application.Platform; 21 | import javafx.stage.Stage; 22 | 23 | public class TestJavaFXRun1 extends Application { 24 | 25 | @Override 26 | public void start(Stage stage) { 27 | System.out.println("JavaFXRun1"); 28 | Platform.exit(); 29 | } 30 | 31 | public static void main(String[] args) { 32 | launch(args); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/test/java/org/openjfx/TestLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020, Gluon 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openjfx; 17 | 18 | public class TestLauncher { 19 | public static void main(String[] args) { 20 | TestJavaFXRun1.main(args); 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/resources/unit/javafxrun-app-test/pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 4.0.0 18 | org.openjfx 19 | javafxrun.basic.test 20 | 1.0-SNAPSHOT 21 | jar 22 | 23 | UTF-8 24 | 11 25 | 11 26 | 12.0.1 27 | 28 | 29 | 30 | org.openjfx 31 | javafx-controls 32 | ${javafx.version} 33 | 34 | 35 | 36 | 37 | 38 | org.openjfx 39 | javafx-maven-plugin 40 | 1.0-SNAPSHOT 41 | 42 | org.openjfx.TestJavaFXRun1 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/test/resources/unit/javafxrun-basic-test/pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 4.0.0 18 | org.openjfx 19 | javafxrun.basic.test 20 | 1.0-SNAPSHOT 21 | jar 22 | 23 | UTF-8 24 | 11 25 | 11 26 | 12.0.1 27 | 28 | 29 | 30 | 31 | org.openjfx 32 | javafx-maven-plugin 33 | 1.0-SNAPSHOT 34 | 35 | org.openjfx.TestJavaFXRun0 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/resources/unit/javafxrun-classpath-test/classpath-launcher.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 4.0.0 18 | org.openjfx 19 | javafxrun.classpath.launcher.test 20 | 1.0-SNAPSHOT 21 | jar 22 | 23 | UTF-8 24 | 11 25 | 11 26 | 15 27 | 28 | 29 | 30 | org.openjfx 31 | javafx-controls 32 | ${javafx.version} 33 | 34 | 35 | 36 | 37 | 38 | org.openjfx 39 | javafx-maven-plugin 40 | 1.0-SNAPSHOT 41 | 42 | org.openjfx.TestLauncher 43 | CLASSPATH 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/unit/javafxrun-classpath-test/classpath-no-launcher.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 4.0.0 18 | org.openjfx 19 | javafxrun.classpath.launcher.test 20 | 1.0-SNAPSHOT 21 | jar 22 | 23 | UTF-8 24 | 11 25 | 11 26 | 15 27 | 28 | 29 | 30 | org.openjfx 31 | javafx-controls 32 | ${javafx.version} 33 | 34 | 35 | 36 | 37 | 38 | org.openjfx 39 | javafx-maven-plugin 40 | 1.0-SNAPSHOT 41 | 42 | org.openjfx.TestJavaFXRun1 43 | CLASSPATH 44 | 45 | 46 | 47 | 48 | 49 | --------------------------------------------------------------------------------