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