View Javadoc
1   /*
2    *    Copyright 2011-2026 the original author or authors.
3    *
4    *    This program is free software; you can redistribute it and/or
5    *    modify it under the terms of the GNU General Public License
6    *    as published by the Free Software Foundation; either version 2
7    *    of the License, or (at your option) any later version.
8    *
9    *    You may obtain a copy of the License at
10   *
11   *       https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
12   *
13   *    This program is distributed in the hope that it will be useful,
14   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
15   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   *    GNU General Public License for more details.
17   */
18  package com.hazendaz.maven.makeself;
19  
20  import java.io.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.util.Arrays;
31  import java.util.List;
32  
33  import javax.inject.Inject;
34  
35  import org.apache.commons.compress.archivers.ArchiveEntry;
36  import org.apache.commons.compress.archivers.ArchiveInputStream;
37  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
38  import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
39  import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
40  import org.apache.maven.plugin.AbstractMojo;
41  import org.apache.maven.plugin.MojoFailureException;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.eclipse.aether.RepositorySystem;
44  import org.eclipse.aether.RepositorySystemSession;
45  import org.eclipse.aether.artifact.Artifact;
46  import org.eclipse.aether.artifact.DefaultArtifact;
47  import org.eclipse.aether.repository.RemoteRepository;
48  import org.eclipse.aether.resolution.ArtifactRequest;
49  import org.eclipse.aether.resolution.ArtifactResolutionException;
50  import org.eclipse.aether.resolution.ArtifactResult;
51  
52  /**
53   * Abstract base class providing portable Git download and installation support for Windows.
54   */
55  public abstract class AbstractGitMojo extends AbstractMojo {
56  
57      /** isWindows is detected at start of plugin to ensure windows needs. */
58      static final boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");
59  
60      /** The Constant GIT_USER_BIN. */
61      static final String GIT_USER_BIN = "/usr/bin/";
62  
63      /**
64       * Returns true if the current platform is Windows. Extracted as a method to allow test subclasses to override the
65       * platform detection without modifying production code.
66       *
67       * @return true if running on Windows
68       */
69      protected boolean isWindows() {
70          return WINDOWS;
71      }
72  
73      /**
74       * The path to existing git install for windows usage. If left blank per default, portable git will be used.
75       * Location should be something like 'C:/Program Files/Git'. When set and not windows, it will be treated as blank.
76       */
77      @Parameter(defaultValue = "", property = "gitPath")
78      protected String gitPath;
79  
80      /** Maven Artifact Factory. */
81      @Inject
82      protected RepositorySystem repositorySystem;
83  
84      /** Maven Repository System Session. */
85      @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true)
86      protected RepositorySystemSession repoSession;
87  
88      /** Maven Remote Repositories. */
89      @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
90      protected List<RemoteRepository> remoteRepositories;
91  
92      /** Portable Git. */
93      protected PortableGit portableGit;
94  
95      /**
96       * Check Git Setup.
97       *
98       * @throws MojoFailureException
99       *             the mojo failure exception
100      */
101     protected void checkGitSetup() throws MojoFailureException {
102         // Get Portable Git Maven Information
103         this.portableGit = new PortableGit(this.getLog());
104 
105         // Extract Portable Git
106         this.extractPortableGit();
107     }
108 
109     /**
110      * Extract Portable Git.
111      *
112      * @throws MojoFailureException
113      *             failure retrieving portable git
114      */
115     protected void extractPortableGit() throws MojoFailureException {
116         final String location = this.repoSession.getLocalRepository().getBasedir() + File.separator
117                 + this.portableGit.getName() + File.separator + this.portableGit.getVersion();
118         if (Files.exists(Path.of(location))) {
119             this.getLog().debug("Existing 'PortableGit' folder found at " + location);
120             this.gitPath = location + AbstractGitMojo.GIT_USER_BIN;
121             return;
122         }
123 
124         this.getLog().info("Loading portable git");
125         final Artifact artifact = new DefaultArtifact(this.portableGit.getGroupId(), this.portableGit.getArtifactId(),
126                 this.portableGit.getClassifier(), this.portableGit.getExtension(), this.portableGit.getVersion());
127         final ArtifactRequest artifactRequest = new ArtifactRequest().setRepositories(this.remoteRepositories)
128                 .setArtifact(artifact);
129         ArtifactResult resolutionResult = null;
130         try {
131             resolutionResult = this.repositorySystem.resolveArtifact(this.repoSession, artifactRequest);
132             if (!resolutionResult.isResolved()) {
133                 throw new MojoFailureException("Unable to resolve artifact: " + artifact.getGroupId() + ":"
134                         + artifact.getArtifactId() + ":" + artifact.getVersion() + ":" + artifact.getClassifier() + ":"
135                         + artifact.getExtension());
136             }
137         } catch (final ArtifactResolutionException e) {
138             throw new MojoFailureException(
139                     "Unable to resolve artifact: " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
140                             + artifact.getVersion() + ":" + artifact.getClassifier() + ":" + artifact.getExtension());
141         }
142         this.installGit(resolutionResult.getArtifact(), location);
143     }
144 
145     /**
146      * Install Git extracts git to .m2/repository under PortableGit.
147      *
148      * @param artifact
149      *            the maven artifact representation for git
150      * @param location
151      *            the location in maven repository to store portable git
152      */
153     protected void installGit(final Artifact artifact, final String location) {
154         Path currentFile = null;
155 
156         // Unzip 'git-for-windows-*-portable.tar.gz' from '.m2/repository/com/github/hazendaz/git/git-for-windows'
157         // into '.m2/repository/PortableGit'
158         try (InputStream inputStream = Files.newInputStream(artifact.getFile().toPath());
159                 InputStream bufferedStream = new BufferedInputStream(inputStream);
160                 InputStream gzipStream = new GzipCompressorInputStream(bufferedStream);
161                 ArchiveInputStream<TarArchiveEntry> tarStream = new TarArchiveInputStream(gzipStream)) {
162             ArchiveEntry entry;
163             final String directory = this.repoSession.getLocalRepository().getBasedir() + File.separator
164                     + this.portableGit.getName();
165             while ((entry = tarStream.getNextEntry()) != null) {
166                 if (entry.isDirectory()) {
167                     continue;
168                 }
169                 currentFile = Path.of(directory, entry.getName());
170                 if (!currentFile.normalize().startsWith(directory)) {
171                     throw new IOException("Bad zip entry, possible directory traversal");
172                 }
173                 final Path parent = currentFile.getParent();
174                 if (!Files.exists(parent)) {
175                     Files.createDirectories(parent);
176                 }
177                 this.getLog().debug("Current file: " + currentFile.getFileName());
178                 Files.copy(tarStream, currentFile, StandardCopyOption.REPLACE_EXISTING);
179             }
180         } catch (final IOException e) {
181             this.getLog().error("", e);
182         }
183 
184         try {
185             if (currentFile != null) {
186                 // Extract Portable Git
187                 this.getLog().debug("Extract Portable Git");
188                 this.runInstaller(Arrays.asList(currentFile.toString(), "-y", "-o", location));
189                 this.gitPath = location + AbstractGitMojo.GIT_USER_BIN;
190             }
191         } catch (final IOException e) {
192             this.getLog().error("", e);
193         } catch (final InterruptedException e) {
194             this.getLog().error("", e);
195             // restore interruption status of the corresponding thread
196             Thread.currentThread().interrupt();
197         }
198     }
199 
200     /**
201      * Run installer executes a process and logs its output.
202      *
203      * @param command
204      *            the command to run
205      *
206      * @throws IOException
207      *             Signals that an I/O exception has occurred.
208      * @throws InterruptedException
209      *             the interrupted exception
210      */
211     protected void runInstaller(final List<String> command) throws IOException, InterruptedException {
212         this.getLog().debug("Execution commands: " + command);
213 
214         final ProcessBuilder processBuilder = new ProcessBuilder(command);
215         processBuilder.redirectErrorStream(true);
216 
217         final Process process = processBuilder.start();
218         try (BufferedReader reader = new BufferedReader(
219                 new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
220             String line = "";
221             while ((line = reader.readLine()) != null) {
222                 this.getLog().info(line);
223             }
224             this.getLog().info("");
225         }
226 
227         final int status = process.waitFor();
228         if (status > 0) {
229             this.getLog().error(String.join(" ", "Process failed with error status:", String.valueOf(status)));
230         }
231     }
232 
233 }