View Javadoc
1   /*
2    *    Copyright 2011-2026 the original author or authors.
3    *
4    *    This program is free software; you can redistribute it and/or
5    *    modify it under the terms of the GNU General Public License
6    *    as published by the Free Software Foundation; either version 2
7    *    of the License, or (at your option) any later version.
8    *
9    *    You may obtain a copy of the License at
10   *
11   *       https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
12   *
13   *    This program is distributed in the hope that it will be useful,
14   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
15   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   *    GNU General Public License for more details.
17   */
18  package com.hazendaz.maven.makeself;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.attribute.PosixFilePermission;
29  import java.nio.file.attribute.PosixFilePermissions;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  
36  import javax.inject.Inject;
37  
38  import org.apache.commons.io.FilenameUtils;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugin.MojoFailureException;
41  import org.apache.maven.plugins.annotations.LifecyclePhase;
42  import org.apache.maven.plugins.annotations.Mojo;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.project.MavenProjectHelper;
46  
47  /**
48   * The Class MakeselfMojo.
49   */
50  @Mojo(name = "makeself", defaultPhase = LifecyclePhase.VERIFY, requiresProject = false)
51  public class MakeselfMojo extends AbstractGitMojo {
52  
53      /** Permissions for makeself script results. */
54      private static final String PERMISSIONS = "rwxr-xr--";
55  
56      /** Static ATTACH_ARTIFACT to maven lifecycle. */
57      private static final boolean ATTACH_ARTIFACT = true;
58  
59      /**
60       * archive_dir is the name of the directory that contains the files to be archived.
61       */
62      @Parameter(defaultValue = "makeself", property = "archiveDir", required = true)
63      private String archiveDir;
64  
65      /**
66       * file_name is the name of the archive to be created.
67       */
68      @Parameter(defaultValue = "makeself.sh", property = "fileName", required = true)
69      private String fileName;
70  
71      /**
72       * label is an arbitrary text string describing the package. It will be displayed while extracting the files.
73       */
74      @Parameter(defaultValue = "Makeself self-extractable archive", property = "label", required = true)
75      private String label;
76  
77      /**
78       * startup_script is the command to be executed from within the directory of extracted files. Thus, if you wish to
79       * execute a program contained in this directory, you must prefix your command with './'. For example, './program'
80       * will be fine.
81       */
82      @Parameter(defaultValue = "./makeself.sh", property = "startupScript", required = true)
83      private String startupScript;
84  
85      /**
86       * extension is for type of fileName being created. It defaults to 'sh' for backwards compatibility. Makeself
87       * defines 'run' as its default, therefore when using 'run', set extension to 'run'. This extension is used when
88       * attaching resulting artifact to maven.
89       *
90       * @since 1.5.0
91       */
92      @Parameter(defaultValue = "sh", property = "extension")
93      private String extension;
94  
95      /**
96       * classifier is for fileName being created to allow for more than one. If not defined, multiple artifacts will all
97       * be installed to same m2 location. The artifact will take on the project artifact where classfier is the physical
98       * name attribute you which to create for the fileName.
99       *
100      * @since 1.5.0
101      */
102     @Parameter(property = "classifier")
103     private String classifier;
104 
105     /**
106      * inline script allows user to skip strict verification of startup script for cases where script is defined
107      * directly such as 'echo hello' where 'echo' is a 'program' to run and 'hello' is one of many 'script arguments'.
108      * Behaviour of makeself plugin prior to 1.5.0 allowed for this undocumented feature which is further allowed and
109      * shown as an example in makeself. Verification therefore checks that both startupScript and scriptArgs exist only.
110      *
111      * @since 1.5.1
112      */
113     @Parameter(property = "inlineScript")
114     private boolean inlineScript;
115 
116     /**
117      * script_args are additional arguments for startup_script passed as an array.
118      *
119      * <pre>
120      * {@code
121      * <scriptArgs>
122      *   <scriptArg>arg1</scriptArg>
123      *   <scriptArg>arg2</scriptArg>
124      * </scriptArgs>
125      * }
126      * </pre>
127      */
128     @Parameter(property = "scriptArgs")
129     private List<String> scriptArgs;
130 
131     /**
132      * --version | -v : Print out Makeself version number and exit
133      *
134      * @since 1.6.0
135      */
136     @Parameter(property = "version")
137     private Boolean version;
138 
139     /**
140      * --help | -h : Print out this help message and exit (exit is custom to makeself maven plugin).
141      */
142     @Parameter(property = "help")
143     private Boolean help;
144 
145     /**
146      * --tar-quietly : Suppress verbose output from the tar command.
147      *
148      * @since 1.6.0
149      */
150     @Parameter(property = "tarQuietly")
151     private Boolean tarQuietly;
152 
153     /**
154      * --quiet | -q : Do not print any messages other than errors.
155      *
156      * @since 1.6.0
157      */
158     @Parameter(property = "quiet")
159     private Boolean quiet;
160 
161     /**
162      * --gzip : Use gzip for compression (the default on platforms on which gzip is commonly available, like Linux).
163      */
164     @Parameter(property = "gzip")
165     private Boolean gzip;
166 
167     /**
168      * --bzip2 : Use bzip2 instead of gzip for better compression. The bzip2 command must be available in the command
169      * path. It is recommended that the archive prefix be set to something like '.bz2.run', so that potential users know
170      * that they'll need bzip2 to extract it.
171      */
172     @Parameter(property = "bzip2")
173     private Boolean bzip2;
174 
175     /**
176      * --bzip3 : Use bzip3 instead of gzip for better compression. The bzip3 command must be available in the command
177      * path. It is recommended that the archive prefix be set to something like '.bz3.run', so that potential users know
178      * that they'll need bzip3 to extract it.
179      *
180      * @since 1.6.0
181      */
182     @Parameter(property = "bzip3")
183     private Boolean bzip3;
184 
185     /**
186      * --pbzip2 : Use pbzip2 instead of gzip for better and faster compression on machines having multiple CPUs. The
187      * pbzip2 command must be available in the command path. It is recommended that the archive prefix be set to
188      * something like '.pbz2.run', so that potential users know that they'll need bzip2 to extract it.
189      */
190     @Parameter(property = "pbzip2")
191     private Boolean pbzip2;
192 
193     /**
194      * --xz : Use xz instead of gzip for better compression. The xz command must be available in the command path. It is
195      * recommended that the archive prefix be set to something like '.xz.run' for the archive, so that potential users
196      * know that they'll need xz to extract it.
197      */
198     @Parameter(property = "xz")
199     private Boolean xz;
200 
201     /**
202      * --lzo : Use lzop instead of gzip for better compression. The lzop command must be available in the command path.
203      * It is recommended that the archive prefix be set to something like '.lzo.run' for the archive, so that potential
204      * users know that they'll need lzop to extract it.
205      */
206     @Parameter(property = "lzo")
207     private Boolean lzo;
208 
209     /**
210      * --lz4 : Use lz4 instead of gzip for better compression. The lz4 command must be available in the command path. It
211      * is recommended that the archive prefix be set to something like '.lz4.run' for the archive, so that potential
212      * users know that they'll need lz4 to extract it.
213      */
214     @Parameter(property = "lz4")
215     private Boolean lz4;
216 
217     /**
218      * --zstd : Use zstd for compression.
219      */
220     @Parameter(property = "zstd")
221     private Boolean zstd;
222 
223     /**
224      * --pigz : Use pigz for compression.
225      */
226     @Parameter(property = "pigz")
227     private Boolean pigz;
228 
229     /**
230      * --base64 : Encode the archive to ASCII in Base64 format (base64 command required).
231      */
232     @Parameter(property = "base64")
233     private Boolean base64;
234 
235     /**
236      * --gpg-encrypt : Encrypt the archive using gpg -ac -z $COMPRESS_LEVEL. This will prompt for a password to encrypt
237      * with. Assumes that potential users have gpg installed.
238      */
239     @Parameter(property = "gpgEncrypt")
240     private Boolean gpgEncrypt;
241 
242     /**
243      * --gpg-asymmetric-encrypt-sign : Instead of compressing, asymmetrically encrypt and sign the data using GPG."
244      */
245     @Parameter(property = "gpgAsymmetricEncryptSign")
246     private Boolean gpgAsymmetricEncryptSign;
247 
248     /**
249      * --ssl-encrypt : Encrypt the archive using openssl aes-256-cbc -a -salt. This will prompt for a password to
250      * encrypt with. Assumes that the potential users have the OpenSSL tools installed.
251      */
252     @Parameter(property = "sslEncrypt")
253     private Boolean sslEncrypt;
254 
255     /**
256      * --ssl-passwd pass : Use the given password to encrypt the data using OpenSSL.
257      */
258     @Parameter(property = "sslPasswd")
259     private String sslPasswd;
260 
261     /**
262      * --ssl-pass-src : Use the given src as the source of password to encrypt the data using OpenSSL. See \"PASS PHRASE
263      * ARGUMENTS\" in man openssl. If this option is not supplied, the user wil be asked to enter encryption pasword on
264      * the current terminal.
265      */
266     @Parameter(property = "sslPassSrc")
267     private String sslPassSrc;
268 
269     /**
270      * --ssl-no-md : Do not use \"-md\" option not supported by older OpenSSL.
271      */
272     @Parameter(property = "sslNoMd")
273     private Boolean sslNoMd;
274 
275     /**
276      * --compress : Use the UNIX compress command to compress the data. This should be the default on all platforms that
277      * don't have gzip available.
278      */
279     @Parameter(property = "compress")
280     private Boolean compress;
281 
282     /**
283      * --complevel : Specify the compression level for gzip, bzip2, bzip3, pbzip2, xz, lzo or lz4. (defaults to 9).
284      */
285     @Parameter(property = "complevel")
286     private Integer complevel;
287 
288     /**
289      * --comp-extra : Append extra options to the chosen compressor.
290      */
291     @Parameter(property = "compExtra")
292     private String compExtra;
293 
294     /**
295      * --nochown : Do not give the target folder to the current user (default)
296      *
297      * @since 1.6.0
298      */
299     @Parameter(property = "nochown")
300     private Boolean nochown;
301 
302     /**
303      * --chown : Give the target folder to the current user recursively
304      *
305      * @since 1.6.0
306      */
307     @Parameter(property = "chown")
308     private Boolean chown;
309 
310     /**
311      * --nocomp : Do not use any compression for the archive, which will then be an uncompressed TAR.
312      */
313     @Parameter(property = "nocomp")
314     private Boolean nocomp;
315 
316     /**
317      * --threads : Specify the number of threads to be used by compressors that support parallelization. Omit to use
318      * compressor's default. Most useful (and required) for opting into xz's threading, usually with --threads=0 for all
319      * available cores. pbzip2 and pigz are parallel by default, and setting this value allows limiting the number of
320      * threads they use.
321      */
322     @Parameter(property = "threads")
323     private Integer threads;
324 
325     /**
326      * --notemp : The generated archive will not extract the files to a temporary directory, but in a new directory
327      * created in the current directory. This is better to distribute software packages that may extract and compile by
328      * themselves (i.e. launch the compilation through the embedded script).
329      */
330     @Parameter(property = "notemp")
331     private Boolean notemp;
332 
333     /**
334      * --needroot : Check that the root user is extracting the archive before proceeding
335      *
336      * @since 1.6.0
337      */
338     @Parameter(property = "needroot")
339     private Boolean needroot;
340 
341     /**
342      * --current : Files will be extracted to the current directory, instead of in a subdirectory. This option implies
343      * --notemp and ddoes not require aq startup_script.
344      */
345     @Parameter(property = "current")
346     private Boolean current;
347 
348     /**
349      * --follow : Follow the symbolic links inside of the archive directory, i.e. store the files that are being pointed
350      * to instead of the links themselves.
351      */
352     @Parameter(property = "follow")
353     private Boolean follow;
354 
355     /**
356      * --noprogress : Do not show the progress during the decompression
357      *
358      * @since 1.6.0
359      */
360     @Parameter(property = "noprogress")
361     private Boolean noprogress;
362 
363     /**
364      * --append (new in 2.1.x): Append data to an existing archive, instead of creating a new one. In this mode, the
365      * settings from the original archive are reused (compression type, label, embedded script), and thus don't need to
366      * be specified again on the command line.
367      */
368     @Parameter(property = "append")
369     private Boolean append;
370 
371     /**
372      * --header: Makeself 2.0 uses a separate file to store the header stub, called makeself-header.sh. By default, it
373      * is assumed that it is stored in the same location as makeself.sh. This option can be used to specify its actual
374      * location if it is stored someplace else. This is not required for this plugin as the header is provided.
375      */
376     @Parameter(property = "headerFile")
377     private String headerFile;
378 
379     /**
380      * --preextract: Specify a pre-extraction script. The script is executed with the same environment and initial
381      * `script_args` as `startup_script`.
382      *
383      * @since 1.7.0
384      */
385     @Parameter(property = "preextractScript")
386     private String preextractScript;
387 
388     /**
389      * --cleanup: Specify a script that is run when execution is interrupted or finishes successfully. The script is
390      * executed with the same environment and initial `script_args` as `startup_script`.
391      */
392     @Parameter(property = "cleanupScript")
393     private String cleanupScript;
394 
395     /**
396      * --copy : Upon extraction, the archive will first extract itself to a temporary directory. The main application of
397      * this is to allow self-contained installers stored in a Makeself archive on a CD, when the installer program will
398      * later need to unmount the CD and allow a new one to be inserted. This prevents "Filesystem busy" errors for
399      * installers that span multiple CDs.
400      */
401     @Parameter(property = "copy")
402     private Boolean copy;
403 
404     /** --nox11 : Disable the automatic spawning of a new terminal in X11. */
405     @Parameter(property = "nox11")
406     private Boolean nox11;
407 
408     /** --nowait : Do not wait for user input after executing embedded program from an xterm. */
409     @Parameter(property = "nowait")
410     private Boolean nowait;
411 
412     /**
413      * --nomd5 : Disable the creation of a MD5 checksum for the archive. This speeds up the extraction process if
414      * integrity checking is not necessary.
415      */
416     @Parameter(property = "nomd5")
417     private Boolean nomd5;
418 
419     /**
420      * --nocrc : Disable the creation of a CRC checksum for the archive. This speeds up the extraction process if
421      * integrity checking is not necessary.
422      */
423     @Parameter(property = "nocrc")
424     private Boolean nocrc;
425 
426     /**
427      * --sha256 : Compute a SHA256 checksum for the archive.
428      */
429     @Parameter(property = "sha256")
430     private Boolean sha256;
431 
432     /**
433      * --sign passphrase : Signature private key to sign the package with.
434      *
435      * @since 1.6.0
436      */
437     @Parameter(property = "signPassphrase")
438     private String signPassphrase;
439 
440     /**
441      * --lsm file : Provide and LSM file to makeself, that will be embedded in the generated archive. LSM files are
442      * describing a software package in a way that is easily parseable. The LSM entry can then be later retrieved using
443      * the --lsm argument to the archive. An example of a LSM file is provided with Makeself.
444      */
445     @Parameter(property = "lsmFile")
446     private String lsmFile;
447 
448     /**
449      * --gpg-extra opt : Append more options to the gpg command line.
450      */
451     @Parameter(property = "gpgExtraOpt")
452     private String gpgExtraOpt;
453 
454     /**
455      * --tar-format opt :Specify the tar archive format (default is ustar); you may use any value accepted by your tar
456      * command (such as posix, v7, etc).
457      */
458     @Parameter(property = "tarFormatOpt")
459     private String tarFormatOpt;
460 
461     /**
462      * --tar-extra opt : Append more options to the tar command line.
463      * <p>
464      * For instance, in order to exclude the .git directory from the packaged archive directory using the GNU tar, one
465      * can use makeself.sh --tar-extra "--exclude=.git" ...
466      */
467     @Parameter(property = "tarExtraOpt")
468     private String tarExtraOpt;
469 
470     /**
471      * --untar-extra opt : Append more options to the during the extraction of the tar archive.
472      */
473     @Parameter(property = "untarExtraOpt")
474     private String untarExtraOpt;
475 
476     /**
477      * --target dir : Specify the directory where the archive will be extracted. This option implies --notemp and does
478      * not require a startup_script.
479      *
480      * @since 1.6.0
481      */
482     private String extractTargetDir;
483 
484     /**
485      * --keep-umask : Keep the umask set to shell default, rather than overriding when executing self-extracting
486      * archive.
487      */
488     @Parameter(property = "keepUmask")
489     private Boolean keepUmask;
490 
491     /**
492      * --export-conf : Export configuration variables to startup_script.
493      */
494     @Parameter(property = "exportConf")
495     private Boolean exportConf;
496 
497     /**
498      * --packaging-date date : Use provided string as the packaging date instead of the current date.
499      */
500     @Parameter(property = "packagingDate")
501     private String packagingDate;
502 
503     /**
504      * --license : Append a license file.
505      */
506     @Parameter(property = "licenseFile")
507     private String licenseFile;
508 
509     /**
510      * --nooverwrite : Do not extract the archive if the specified target directory already exists.
511      */
512     @Parameter(property = "nooverwrite")
513     private Boolean nooverwrite;
514 
515     /**
516      * --help-header file : Add a header to the archive's --help output.
517      */
518     @Parameter(property = "helpHeaderFile")
519     private String helpHeaderFile;
520 
521     /** Skip run of plugin. */
522     @Parameter(defaultValue = "false", property = "makeself.skip")
523     private boolean skip;
524 
525     /** Auto run : When set to true, resulting shell will be run. This is useful for testing purposes. */
526     @Parameter(defaultValue = "false", property = "autoRun")
527     private boolean autoRun;
528 
529     /** The build target. */
530     @Parameter(defaultValue = "${project.build.directory}/", readonly = true)
531     private String buildTarget;
532 
533     /** The makeself temp directory. */
534     @Parameter(defaultValue = "${project.build.directory}/makeself-tmp/", readonly = true)
535     private File makeselfTempDirectory;
536 
537     /** Maven ProjectHelper. */
538     @Inject
539     private MavenProjectHelper projectHelper;
540 
541     /** Maven Project. */
542     @Parameter(defaultValue = "${project}", readonly = true, required = true)
543     private MavenProject project;
544 
545     /** The makeself. */
546     private Path makeself;
547 
548     @Override
549     public void execute() throws MojoExecutionException, MojoFailureException {
550         // Ensure gitPath is never null
551         if (this.gitPath == null) {
552             this.gitPath = "";
553         }
554 
555         // Check if plugin run should be skipped
556         if (this.skip) {
557             this.getLog().info("Makeself is skipped");
558             return;
559         }
560 
561         // Validate archive directory exists
562         Path path = Path.of(this.buildTarget.concat(this.archiveDir));
563         if (!Files.exists(path)) {
564             throw new MojoExecutionException("ArchiveDir: missing '" + this.buildTarget.concat(this.archiveDir) + "'");
565         }
566 
567         // Validate inline script or startup script file
568         if (this.inlineScript) {
569             // Validate inline script has script args
570             if (this.scriptArgs == null) {
571                 throw new MojoExecutionException("ScriptArgs required when running inlineScript");
572             }
573         } else {
574             // Validate startupScript file starts with './'
575             if (!this.startupScript.startsWith("./")) {
576                 throw new MojoExecutionException("StartupScript required to start with './'");
577             }
578 
579             // Validate startupScript file exists
580             path = Path.of(this.buildTarget.concat(this.archiveDir).concat(this.startupScript.substring(1)));
581             if (!Files.exists(path)) {
582                 throw new MojoExecutionException("StartupScript: missing '"
583                         + this.buildTarget.concat(this.archiveDir).concat(this.startupScript.substring(1)) + "'");
584             }
585         }
586 
587         // Setup make self files
588         this.extractMakeself();
589 
590         // Check git setup
591         if (this.isWindows()) {
592             if (!this.gitPath.isEmpty() && Files.exists(Path.of(this.gitPath))) {
593                 this.getLog().debug("Using existing 'Git' found at " + this.gitPath);
594                 this.gitPath = this.gitPath + AbstractGitMojo.GIT_USER_BIN;
595             } else {
596                 this.checkGitSetup();
597             }
598         } else {
599             // Do not use git path when not windows
600             this.gitPath = "";
601         }
602 
603         try {
604             // Output version of bash
605             this.getLog().debug("Execute Bash Version");
606             this.execute(Arrays.asList(this.gitPath + "bash", "--version"), !MakeselfMojo.ATTACH_ARTIFACT);
607 
608             // Output version of makeself.sh
609             this.getLog().debug("Execute Makeself Version");
610             this.execute(Arrays.asList(this.gitPath + "bash", this.makeself.toAbsolutePath().toString(), "--version"),
611                     !MakeselfMojo.ATTACH_ARTIFACT);
612 
613             // If version arguments supplied, exit as we just printed version.
614             if (this.isTrue(this.version)) {
615                 return;
616             }
617 
618             // If help arguments supplied, write output and get out of code.
619             if (this.isTrue(this.help)) {
620                 this.getLog().debug("Execute Makeself Help");
621                 this.execute(Arrays.asList(this.gitPath + "bash", this.makeself.toAbsolutePath().toString(), "--help"),
622                         !MakeselfMojo.ATTACH_ARTIFACT);
623                 return;
624             }
625 
626             // Basic Configuration
627             this.getLog().debug("Loading Makeself Basic Configuration");
628             final List<String> target = new ArrayList<>(
629                     Arrays.asList(this.gitPath + "bash", this.makeself.toAbsolutePath().toString()));
630             target.addAll(this.loadArgs());
631             target.add(this.buildTarget.concat(this.archiveDir));
632             target.add(this.buildTarget.concat(this.fileName));
633             target.add(this.label);
634             target.add(this.startupScript);
635             if (this.scriptArgs != null) {
636                 target.addAll(this.scriptArgs);
637             }
638 
639             // Indicate makeself running
640             this.getLog().info("Running makeself build");
641 
642             // Execute main run of makeself.sh
643             this.getLog().debug("Execute Makeself Build");
644             this.execute(target, MakeselfMojo.ATTACH_ARTIFACT);
645 
646             // Output info on file makeself created
647             this.getLog().debug("Execute Makeself Info on Resulting Shell Script");
648             this.execute(Arrays.asList(this.gitPath + "bash", this.buildTarget.concat(this.fileName), "--info"),
649                     !MakeselfMojo.ATTACH_ARTIFACT);
650 
651             // Output list on file makeself created (non windows need)
652             if (!this.isWindows()) {
653                 this.getLog().debug("Execute Makeself List on Resulting Shell Script");
654                 this.execute(Arrays.asList(this.gitPath + "bash", this.buildTarget.concat(this.fileName), "--list"),
655                         !MakeselfMojo.ATTACH_ARTIFACT);
656             }
657 
658             // auto run script
659             if (this.autoRun) {
660                 this.getLog().info("Auto-run created shell (this may take a few minutes)");
661                 this.execute(Arrays.asList(this.gitPath + "bash", this.buildTarget.concat(this.fileName)),
662                         !MakeselfMojo.ATTACH_ARTIFACT);
663             }
664         } catch (final IOException e) {
665             this.getLog().error("", e);
666         } catch (final InterruptedException e) {
667             this.getLog().error("", e);
668             // restore interruption status of the corresponding thread
669             Thread.currentThread().interrupt();
670         }
671     }
672 
673     private void execute(final List<String> target, final boolean attach) throws IOException, InterruptedException {
674 
675         // Log execution target
676         this.getLog().debug("Execution commands: " + target);
677 
678         // Create Process Builder
679         final ProcessBuilder processBuilder = new ProcessBuilder(target);
680         processBuilder.redirectErrorStream(true);
681 
682         // Add portable git to windows environment
683         if (this.isWindows()) {
684             final Map<String, String> envs = processBuilder.environment();
685             this.getLog().debug("Environment Variables: " + envs);
686 
687             if (this.portableGit == null) {
688                 // Helper as git located in provided location (not real user or system path, just the process)
689                 final String location = this.gitPath;
690                 // Windows cmd/powershell shows "Path" in this case
691                 if (envs.get("Path") != null) {
692                     envs.put("Path", location + ";" + envs.get("Path"));
693                     this.getLog().debug("Environment Path Variable: " + envs.get("Path"));
694                 } else if (envs.get("PATH") != null) {
695                     // Windows bash shows "PATH" in this case and has issues with spacing as in 'Program Files'
696                     envs.put("PATH", location + ";" + envs.get("PATH").replace("Program Files", "\"Program Files\""));
697                     this.getLog().debug("Environment Path Variable: " + envs.get("PATH"));
698                 }
699             } else {
700                 // Helper as portable git located in .m2 (not real user or system path, just the process)
701                 final String location = this.repoSession.getLocalRepository().getBasedir() + File.separator
702                         + this.portableGit.getName() + File.separator + this.portableGit.getVersion();
703                 // Windows cmd/powershell shows "Path" in this case
704                 if (envs.get("Path") != null) {
705                     envs.put("Path", location + "/usr/bin;" + envs.get("Path"));
706                     this.getLog().debug("Environment Path Variable: " + envs.get("Path"));
707                 } else if (envs.get("PATH") != null) {
708                     // Windows bash shows "PATH" in this case and has issues with spacing as in 'Program Files'
709                     envs.put("PATH",
710                             location + "/usr/bin;" + envs.get("PATH").replace("Program Files", "\"Program Files\""));
711                     this.getLog().debug("Environment Path Variable: " + envs.get("PATH"));
712                 }
713             }
714         }
715 
716         // Create Process
717         final Process process = processBuilder.start();
718 
719         // Write process output
720         try (BufferedReader reader = new BufferedReader(
721                 new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
722             String line = "";
723             while ((line = reader.readLine()) != null) {
724                 this.getLog().info(line);
725             }
726             this.getLog().info("");
727         }
728 
729         // Wait for process completion
730         final int status = process.waitFor();
731         if (status > 0) {
732             this.getLog().error(String.join(" ", "makeself failed with error status:", String.valueOf(status)));
733         }
734 
735         // Attach artifact to maven build for install/deploy/release on success
736         if (status == 0 && attach) {
737             this.projectHelper.attachArtifact(this.project, this.extension, this.classifier,
738                     Path.of(this.buildTarget, FilenameUtils.getName(this.fileName)).toFile());
739         }
740     }
741 
742     /**
743      * Extract makeself.
744      */
745     private void extractMakeself() {
746         this.getLog().debug("Extracting Makeself");
747 
748         // Create makeself directory
749         final Path makeselfTemp = Path.of(this.makeselfTempDirectory.getAbsolutePath());
750         if (!Files.exists(makeselfTemp) && !makeselfTemp.toFile().mkdirs()) {
751             this.getLog()
752                     .error(String.join(" ", "Unable to make directory", this.makeselfTempDirectory.getAbsolutePath()));
753             return;
754         }
755         this.getLog().debug(String.join(" ", "Created directory for", this.makeselfTempDirectory.getAbsolutePath()));
756 
757         final ClassLoader classloader = this.getClass().getClassLoader();
758 
759         // Write makeself script
760         this.makeself = this.makeselfTempDirectory.toPath().resolve("makeself.sh");
761         if (!Files.exists(this.makeself)) {
762             this.getLog().debug("Writing makeself.sh");
763             try (InputStream link = classloader.getResourceAsStream("META-INF/makeself/makeself.sh")) {
764                 final Path path = this.makeself.toAbsolutePath();
765                 Files.copy(link, path);
766                 this.setFilePermissions(this.makeself.toFile());
767                 this.setPosixFilePermissions(path);
768             } catch (final IOException e) {
769                 this.getLog().error("", e);
770             }
771         }
772 
773         // Write makeself-header script
774         final Path makeselfHeader = this.makeselfTempDirectory.toPath().resolve("makeself-header.sh");
775         if (!Files.exists(makeselfHeader)) {
776             this.getLog().debug("Writing makeself-header.sh");
777             try (InputStream link = classloader.getResourceAsStream("META-INF/makeself/makeself-header.sh")) {
778                 final Path path = makeselfHeader.toAbsolutePath();
779                 Files.copy(link, path);
780                 this.setFilePermissions(makeselfHeader.toFile());
781                 this.setPosixFilePermissions(path);
782             } catch (final IOException e) {
783                 this.getLog().error("", e);
784             }
785         }
786     }
787 
788     private void setFilePermissions(final File file) {
789         if (!file.setExecutable(true, true)) {
790             this.getLog().error(String.join(" ", "Unable to set executable:", file.getName()));
791         } else {
792             this.getLog().debug(String.join(" ", "Set executable for", file.getName()));
793         }
794     }
795 
796     private void setPosixFilePermissions(final Path path) {
797         final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(MakeselfMojo.PERMISSIONS);
798 
799         try {
800             Files.setPosixFilePermissions(path, permissions);
801             this.getLog().debug(String.join(" ", "Set Posix File Permissions for", path.toString(), "as",
802                     MakeselfMojo.PERMISSIONS));
803         } catch (final IOException e) {
804             this.getLog().error("Failed attempted Posix permissions", e);
805         } catch (final UnsupportedOperationException e) {
806             // Attempting but don't care about status if it fails
807             this.getLog().debug("Failed attempted Posix permissions", e);
808         }
809     }
810 
811     /**
812      * Load args.
813      *
814      * @return the string
815      */
816     private List<String> loadArgs() {
817         this.getLog().debug("Loading arguments");
818 
819         final List<String> args = new ArrayList<>(50);
820 
821         // " --tar-quietly : Suppress verbose output from the tar command"
822         if (this.isTrue(this.tarQuietly)) {
823             args.add("--tar-quietly");
824         }
825 
826         // " --quiet | -q : Do not print any messages other than errors."
827         if (this.isTrue(this.quiet)) {
828             args.add("--quiet");
829         }
830 
831         // --gzip : Use gzip for compression (the default on platforms on which gzip is commonly available, like Linux)
832         if (this.isTrue(this.gzip)) {
833             args.add("--gzip");
834         }
835 
836         // --bzip2 : Use bzip2 instead of gzip for better compression. The bzip2 command must be available in the
837         // command path. It is recommended that the archive prefix be set to something like '.bz2.run', so that
838         // potential users know that they'll need bzip2 to extract it.
839         if (this.isTrue(this.bzip2)) {
840             args.add("--bzip2");
841         }
842 
843         // --bzip3 : Use bzip3 instead of gzip for better compression. The bzip3 command must be available in the
844         // command path. It is recommended that the archive prefix be set to something like '.bz3.run', so that
845         // potential users know that they'll need bzip3 to extract it.
846         if (this.isTrue(this.bzip3)) {
847             args.add("--bzip3");
848         }
849 
850         // --pbzip2 : Use pbzip2 instead of gzip for better and faster compression on machines having multiple CPUs.
851         // The pbzip2 command must be available in the command path. It is recommended that the archive prefix be
852         // set to something like '.pbz2.run', so that potential users know that they'll need bzip2 to extract it.
853         if (this.isTrue(this.pbzip2)) {
854             args.add("--pbzip2");
855         }
856 
857         // --xz : Use xz instead of gzip for better compression. The xz command must be available in the command path.
858         // It is recommended that the archive prefix be set to something like '.xz.run' for the archive, so that
859         // potential users know that they'll need xz to extract it.
860         if (this.isTrue(this.xz)) {
861             args.add("--xz");
862         }
863 
864         // --lzo : Use lzop instead of gzip for better compression. The lzop command must be available in the command
865         // path. It is recommended that the archive prefix be set to something like '.lzo.run' for the archive, so
866         // that potential users know that they'll need lzop to extract it.
867         if (this.isTrue(this.lzo)) {
868             args.add("--lzo");
869         }
870 
871         // --lz4 : Use lz4 instead of gzip for better compression. The lz4 command must be available in the command
872         // path. It is recommended that the archive prefix be set to something like '.lz4.run' for the archive, so
873         // that potential users know that they'll need lz4 to extract it.
874         if (this.isTrue(this.lz4)) {
875             args.add("--lz4");
876         }
877 
878         // --zstd : Use zstd for compression.
879         if (this.isTrue(this.zstd)) {
880             args.add("--zstd");
881         }
882 
883         // --pigz : Use pigz for compression.
884         if (this.isTrue(this.pigz)) {
885             args.add("--pigz");
886         }
887 
888         // --base64 : Encode the archive to ASCII in Base64 format (base64 command required).
889         if (this.isTrue(this.base64)) {
890             args.add("--base64");
891         }
892 
893         // --gpg-encrypt : Encrypt the archive using gpg -ac -z $COMPRESS_LEVEL. This will prompt for a password to
894         // encrypt with. Assumes that potential users have gpg installed.
895         if (this.isTrue(this.gpgEncrypt)) {
896             args.add("--gpg-encrypt");
897         }
898 
899         // --gpg-asymmetric-encrypt-sign : Instead of compressing, asymmetrically encrypt and sign the data using GPG
900         if (this.isTrue(this.gpgAsymmetricEncryptSign)) {
901             args.add("--gpg-asymmetric-encrypt-sign");
902         }
903 
904         // --ssl-encrypt : Encrypt the archive using openssl aes-256-cbc -a -salt. This will prompt for a password to
905         // encrypt with. Assumes that the potential users have the OpenSSL tools installed.
906         if (this.isTrue(this.sslEncrypt)) {
907             args.add("--ssl-encrypt");
908         }
909 
910         // --ssl-passwd pass : Use the given password to encrypt the data using OpenSSL.
911         if (this.sslPasswd != null) {
912             args.add("--ssl-passwd");
913             args.add(this.sslPasswd);
914         }
915 
916         // --ssl-pass-src src : Use the given src as the source of password to encrypt the data using OpenSSL. See
917         // \"PASS PHRASE ARGUMENTS\" in man openssl. If this option is not supplied, the user wil be asked to enter
918         // encryption pasword on the current terminal.
919         if (this.sslPassSrc != null) {
920             args.add("--ssl-pass-src");
921             args.add(this.sslPassSrc);
922         }
923 
924         // --ssl-no-md : Do not use \"-md\" option not supported by older OpenSSL.
925         if (this.isTrue(this.sslNoMd)) {
926             args.add("--ssl-no-md");
927         }
928 
929         // --compress : Use the UNIX compress command to compress the data. This should be the default on all platforms
930         // that don't have gzip available.
931         if (this.isTrue(this.compress)) {
932             args.add("--compress");
933         }
934 
935         // --complevel : Specify the compression level for gzip, bzip2, bzip3, pbzip2, xz, lzo or lz4. (defaults to 9)
936         if (this.complevel != null) {
937             args.add("--complevel");
938             args.add(this.complevel.toString());
939         }
940 
941         // --comp-extra : Append extra options to the chosen compressor"
942         if (this.compExtra != null) {
943             args.add("--comp-extra");
944             args.add(this.compExtra);
945         }
946 
947         // --nochown : Do not give the target folder to the current user (default)
948         if (this.isTrue(this.nochown)) {
949             args.add("--nochown");
950         }
951 
952         // --chown : Give the target folder to the current user recursively.
953         if (this.isTrue(this.chown)) {
954             args.add("--chown");
955         }
956 
957         // --nocomp : Do not use any compression for the archive, which will then be an uncompressed TAR.
958         if (this.isTrue(this.nocomp)) {
959             args.add("--nocomp");
960         }
961 
962         // --threads thds : Number of threads to be used by compressors that support parallelization.
963         // Omit to use compressor's default. Most useful (and required) for opting into xz's threading,
964         // usually with '--threads=0' for all available cores.pbzip2 and pigz are parallel by default,
965         // and setting this value allows limiting the number of threads they use.
966         if (this.threads != null) {
967             args.add("--threads");
968             args.add(this.threads.toString());
969         }
970 
971         // --notemp : The generated archive will not extract the files to a temporary directory, but in a new directory
972         // created in the current directory. This is better to distribute software packages that may extract and compile
973         // by themselves (i.e. launch the compilation through the embedded script).
974         if (this.isTrue(this.notemp)) {
975             args.add("--notemp");
976         }
977 
978         // --needroot : Check that the root user is extracting the archive before proceeding
979         if (this.isTrue(this.needroot)) {
980             args.add("--needroot");
981         }
982 
983         // --current : Files will be extracted to the current directory, instead of in a sub-directory. This option
984         // implies --notemp and does not require a startup_script.
985         if (this.isTrue(this.current)) {
986             args.add("--current");
987         }
988 
989         // --follow : Follow the symbolic links inside of the archive directory, i.e. store the files that are being
990         // pointed to instead of the links themselves.
991         if (this.isTrue(this.follow)) {
992             args.add("--follow");
993         }
994 
995         // --noprogress : Do not show the progress during the decompression
996         if (this.isTrue(this.noprogress)) {
997             args.add("--noprogress");
998         }
999 
1000         // --append (new in 2.1.x): Append data to an existing archive, instead of creating a new one. In this mode, the
1001         // settings from the original archive are reused (compression type, label, embedded script), and thus don't need
1002         // to be specified again on the command line.
1003         if (this.isTrue(this.append)) {
1004             args.add("--append");
1005         }
1006 
1007         // --header : Makeself 2.0 uses a separate file to store the header stub, called makeself-header.sh. By default,
1008         // it is assumed that it is stored in the same location as makeself.sh. This option can be used to specify its
1009         // actual location if it is stored someplace else.
1010         if (this.headerFile != null) {
1011             args.add("--header");
1012             args.add(this.headerFile);
1013         }
1014 
1015         // --preextract : Specify a pre-extraction script. The script is executed with the same environment and initial
1016         // `script_args` as `startup_script`.
1017         if (this.preextractScript != null) {
1018             args.add("--preextract");
1019             args.add(this.preextractScript);
1020         }
1021 
1022         // --cleanup : Specify a script that is run when execution is interrupted or finishes successfully. The script
1023         // is executed with the same environment and initial `script_args` as `startup_script`.
1024         if (this.cleanupScript != null) {
1025             args.add("--cleanup");
1026             args.add(this.cleanupScript);
1027         }
1028 
1029         // --copy : Upon extraction, the archive will first extract itself to a temporary directory. The main
1030         // application of this is to allow self-contained installers stored in a Makeself archive on a CD, when the
1031         // installer program will later need to unmount the CD and allow a new one to be inserted. This prevents
1032         // "File system busy" errors for installers that span multiple CDs.
1033         if (this.isTrue(this.copy)) {
1034             args.add("--copy");
1035         }
1036 
1037         // --nox11 : Disable the automatic spawning of a new terminal in X11.
1038         if (this.isTrue(this.nox11)) {
1039             args.add("--nox11");
1040         }
1041 
1042         // --nowait : When executed from a new X11 terminal, disable the user prompt at the end of the script execution.
1043         if (this.isTrue(this.nowait)) {
1044             args.add("--nowait");
1045         }
1046 
1047         // --nomd5 : Disable the creation of a MD5 checksum for the archive. This speeds up the extraction process if
1048         // integrity checking is not necessary.
1049         if (this.isTrue(this.nomd5)) {
1050             args.add("--nomd5");
1051         }
1052 
1053         // --nocrc : Disable the creation of a CRC checksum for the archive. This speeds up the extraction process if
1054         // integrity checking is not necessary.
1055         if (this.isTrue(this.nocrc)) {
1056             args.add("--nocrc");
1057         }
1058 
1059         // --sha256 : Compute a SHA256 checksum for the archive.
1060         if (this.isTrue(this.sha256)) {
1061             args.add("--sha256");
1062         }
1063 
1064         // --lsm file : Provide and LSM file to makeself, that will be embedded in the generated archive. LSM files are
1065         // describing a software package in a way that is easily parseable. The LSM entry can then be later retrieved
1066         // using the --lsm argument to the archive. An example of a LSM file is provided
1067         // with Makeself.
1068         if (this.lsmFile != null) {
1069             args.add("--lsm");
1070             args.add(this.lsmFile);
1071         }
1072 
1073         // --gpg-extra opt : Append more options to the gpg command line.
1074         if (this.gpgExtraOpt != null) {
1075             args.add("--gpg-extra");
1076             args.add(this.gpgExtraOpt);
1077         }
1078 
1079         // --tar-format opt : Specify the tar archive format (default is ustar); you may use any value accepted by your
1080         // tar command (such as posix, v7, etc).
1081         if (this.tarFormatOpt != null) {
1082             args.add("--tar-format");
1083             args.add(this.tarFormatOpt);
1084         }
1085 
1086         // --tar-extra opt : Append more options to the tar command line.
1087         // For instance, in order to exclude the .git directory from the packaged archive directory using the GNU tar,
1088         // one can use makeself.sh --tar-extra "--exclude=.git" ...
1089         if (this.tarExtraOpt != null) {
1090             args.add("--tar-extra");
1091             args.add(this.tarExtraOpt);
1092         }
1093 
1094         // --untar-extra opt : Append more options to the during the extraction of the tar archive.
1095         if (this.untarExtraOpt != null) {
1096             args.add("--untar-extra");
1097             args.add(this.untarExtraOpt);
1098         }
1099 
1100         // --sign passphrase : Signature private key to sign the package with
1101         if (this.signPassphrase != null) {
1102             args.add("--sign");
1103             args.add(this.signPassphrase);
1104         }
1105 
1106         // --target dir : Specify the directory where the archive will be extracted. This option implies
1107         // --notemp and does not require a startup_script.
1108         if (this.extractTargetDir != null) {
1109             args.add("--target");
1110             args.add(this.extractTargetDir);
1111         }
1112 
1113         // --keep-umask : Keep the umask set to shell default, rather than overriding when executing self-extracting
1114         // archive.
1115         if (this.isTrue(this.keepUmask)) {
1116             args.add("--keep-umask");
1117         }
1118 
1119         // --export-conf : Export configuration variables to startup_script"
1120         if (this.isTrue(this.exportConf)) {
1121             args.add("--export-conf");
1122         }
1123 
1124         // --packaging-date date : Use provided string as the packaging date instead of the current date.
1125         if (this.packagingDate != null) {
1126             args.add("--packaging-date");
1127             args.add(this.packagingDate);
1128         }
1129 
1130         // --license : Append a license file.
1131         if (this.licenseFile != null) {
1132             args.add("--license");
1133             args.add(this.licenseFile);
1134         }
1135 
1136         // --nooverwrite : Do not extract the archive if the specified target directory already exists.
1137         if (this.isTrue(this.nooverwrite)) {
1138             args.add("--nooverwrite");
1139         }
1140 
1141         // --help-header file : Add a header to the archive's --help output.
1142         if (this.helpHeaderFile != null) {
1143             args.add("--help-header");
1144             args.add(this.helpHeaderFile);
1145         }
1146 
1147         return args;
1148     }
1149 
1150     private boolean isTrue(final Boolean value) {
1151         if (value != null) {
1152             return value.booleanValue();
1153         }
1154         return false;
1155     }
1156 
1157 }