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