View Javadoc
1   /*-
2    * #%L
3    * io.earcam.maven.plugin.ramdisk
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    *
9    * You <b>must</b> choose to accept, in full - any individual or combination of
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
18   */
19  package io.earcam.maven.plugin.ramdisk;
20  
21  import static io.earcam.maven.plugin.ramdisk.RamdiskBuildExtension.NAME;
22  import static io.earcam.maven.plugin.ramdisk.RamdiskMojo.PROPERTY_DIRECTORY;
23  import static io.earcam.maven.plugin.ramdisk.RamdiskMojo.PROPERTY_SKIP;
24  import static java.nio.charset.Charset.defaultCharset;
25  import static java.nio.charset.StandardCharsets.UTF_8;
26  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.util.ArrayList;
34  import java.util.Collections;
35  import java.util.List;
36  import java.util.Objects;
37  import java.util.Optional;
38  import java.util.Properties;
39  import java.util.Scanner;
40  
41  import org.apache.maven.AbstractMavenLifecycleParticipant;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.model.Plugin;
44  import org.apache.maven.model.PluginExecution;
45  import org.apache.maven.model.Scm;
46  import org.apache.maven.project.MavenProject;
47  import org.codehaus.plexus.component.annotations.Component;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import io.earcam.unexceptional.Exceptional;
52  import io.earcam.utilitarian.io.IoStreams;
53  import io.earcam.utilitarian.io.file.RecursiveFiles;
54  
55  @Component(role = AbstractMavenLifecycleParticipant.class, hint = NAME, instantiationStrategy = "singleton")
56  public class RamdiskBuildExtension extends AbstractMavenLifecycleParticipant {
57  
58  	private static final String TMPFS_NOT_FOUND = "Could not determine tmpfs directory to use for target";
59  	private static final Logger LOG = LoggerFactory.getLogger(RamdiskBuildExtension.class);
60  
61  	static final String NAME = "ramdisk";
62  	static final String LOG_CATEGORY = "[ramdisk-extension]";
63  
64  	Path tmpFsRoot;
65  	private Plugin plugin;
66  
67  
68  	@Override
69  	public void afterProjectsRead(MavenSession session)
70  	{
71  		Properties userProperties = session.getUserProperties();
72  		if(shouldSkip(userProperties, LOG_CATEGORY)) {
73  			return;
74  		}
75  		userProperties.put("clean.followSymLinks", "true");
76  		userProperties.put("maven.clean.followSymLinks", "true");
77  
78  		tmpFsRoot = selectTmpFs(session.getCurrentProject());
79  		if(tmpFsRoot == null) {
80  			LOG.warn("{} {}", LOG_CATEGORY, TMPFS_NOT_FOUND);
81  			return;
82  		}
83  		List<MavenProject> modules = session.getAllProjects();
84  		LOG.debug("{} Applying ramdisk for: {}", LOG_CATEGORY, modules);
85  
86  		plugin = createPlugin();
87  
88  		modules.forEach(this::createTmpFsBuildDir);
89  	}
90  
91  
92  	static boolean shouldSkip(Properties properties, String logCategory)
93  	{
94  		boolean skip = "true".equals(properties.getOrDefault(PROPERTY_SKIP, "false"));
95  		if(skip) {
96  			LOG.debug("{} configured to skip execution", logCategory);
97  		}
98  		return skip;
99  	}
100 
101 
102 	protected Path selectTmpFs(MavenProject project)
103 	{
104 		return tmpFsFor(project);
105 	}
106 
107 
108 	static Path tmpFsFor(MavenProject project)
109 	{
110 		Optional<Path> fromProperty = fromProperty(project.getProperties());
111 		return fromProperty.orElse(findTmpFs());
112 	}
113 
114 
115 	private static Optional<Path> fromProperty(Properties properties)
116 	{
117 		return Optional.ofNullable(properties.getProperty(PROPERTY_DIRECTORY, System.getProperty(PROPERTY_DIRECTORY)))
118 				.map(File::new)
119 				.map(File::toPath);
120 	}
121 
122 
123 	static Path findTmpFs()
124 	{
125 		String uid = extractUid();
126 
127 		List<Path> possibilities = new ArrayList<>();
128 		if(uid != null) {
129 			possibilities.add(Paths.get("/", "run", uid));
130 			possibilities.add(Paths.get("/", "run", "user", uid));
131 			possibilities.add(Paths.get("/", "var", "run", "user", uid));
132 		}
133 		possibilities.add(Paths.get("/", "dev", "shm"));
134 		possibilities.add(Paths.get("/", "tmp"));
135 
136 		return possibilities.stream()
137 				.sequential()
138 				.filter(Files::isWritable)
139 				.findFirst()
140 				.orElse(null);
141 	}
142 
143 
144 	private static String extractUid()
145 	{
146 		try {
147 			Process process = new ProcessBuilder("/usr/bin/id", "-u", System.getProperty("user.name")).redirectErrorStream(true).start();
148 
149 			try(Scanner scanner = new Scanner(process.getInputStream(), defaultCharset().toString())) {
150 				return Integer.toString(scanner.nextInt());
151 			}
152 		} catch(Exception e) {
153 			LOG.debug("{} Unable to get UID", LOG_CATEGORY, e);
154 			return null;
155 		}
156 	}
157 
158 
159 	private Plugin createPlugin()
160 	{
161 		String scrapeDeets = "META-INF/maven/io.earcam.maven.plugin/io.earcam.maven.plugin.ramdisk/plugin-help.xml";
162 		String xml = new String(IoStreams.readAllBytes(getClass().getClassLoader().getResourceAsStream(scrapeDeets)), UTF_8);
163 
164 		Plugin plugin = new Plugin();
165 		plugin.setGroupId(extractFromXml(xml, "<groupId>", "</groupId>"));
166 		plugin.setArtifactId(extractFromXml(xml, "<artifactId>", "</artifactId>"));
167 		plugin.setVersion(extractFromXml(xml, "<version>", "</version>"));
168 
169 		PluginExecution exec = new PluginExecution();
170 		exec.setId("post-clean");
171 		exec.setGoals(Collections.singletonList("ramdisk"));
172 		exec.setPhase("post-clean");
173 		plugin.addExecution(exec);
174 
175 		exec = new PluginExecution();
176 		exec.setId("validate");
177 		exec.setGoals(Collections.singletonList("ramdisk"));
178 		exec.setPhase("validate");
179 		plugin.addExecution(exec);
180 		return plugin;
181 	}
182 
183 
184 	private String extractFromXml(String xml, String openTag, String closeTag)
185 	{
186 		int start = xml.indexOf(openTag);
187 		int end = xml.indexOf(closeTag);
188 		return xml.substring(start + openTag.length(), end);
189 	}
190 
191 
192 	private void createTmpFsBuildDir(MavenProject project)
193 	{
194 		if(shouldSkip(project.getProperties(), LOG_CATEGORY)) {
195 			return;
196 		}
197 		project.getBuild().addPlugin(plugin);
198 		Exceptional.accept(RamdiskBuildExtension::createTmpFsBuildDir, project, tmpFsRoot);
199 	}
200 
201 
202 	static void createTmpFsBuildDir(MavenProject project, Path tmpFs) throws IOException
203 	{
204 		Objects.requireNonNull(tmpFs, TMPFS_NOT_FOUND);
205 
206 		Path linkTarget = tmpFs.resolve(relativePathFor(project));
207 
208 		if(!linkTarget.toFile().exists()) {
209 			LOG.debug("link target does not exist, creating: {}", linkTarget);
210 			Files.createDirectories(linkTarget);
211 		}
212 
213 		Path link = Paths.get(project.getBuild().getDirectory());
214 
215 		if(link.toFile().exists() && !Files.isSymbolicLink(link)) {
216 			LOG.debug("{} local target directory exists but is not a symbolic link, moving contents: {}", LOG_CATEGORY, link);
217 			RecursiveFiles.move(link, linkTarget, REPLACE_EXISTING);
218 
219 			// TODO what-if: link exists but points somewhere else ...
220 			// use-case, someone switches branches
221 			// we'll need to check: (!Files.readSymbolicLink(linkSource).equals(linkTarget.toAbsolutePath()))
222 		}
223 		if(!link.toFile().exists()) {
224 			LOG.debug("{} link does not exist, creating: {}", LOG_CATEGORY, link);
225 			Files.createSymbolicLink(link, linkTarget);
226 		}
227 	}
228 
229 
230 	static Path relativePathFor(MavenProject project)
231 	{
232 		Path linkTarget = Paths.get("maven", project.getGroupId(), project.getArtifactId(), project.getVersion());
233 		return appendScmTag(project, linkTarget);
234 	}
235 
236 
237 	private static Path appendScmTag(MavenProject project, Path linkTarget)
238 	{
239 		Scm scm = project.getScm();
240 		if(scm != null) {
241 			String tag = scm.getTag();
242 			if(tag != null && !tag.isEmpty()) {
243 				linkTarget = linkTarget.resolve(tag);
244 			}
245 		}
246 		return linkTarget;
247 	}
248 
249 
250 	/**
251 	 * This ensures that softlinks are recreated to avoid things like IDEs (that
252 	 * may be oblivious to extensions) creating "target" directories after a clean.
253 	 */
254 	@Override
255 	public void afterSessionEnd(MavenSession session)
256 	{
257 		session.getAllProjects().forEach(this::createTmpFsBuildDir);
258 	}
259 }