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