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