1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
220
221
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
252
253
254 @Override
255 public void afterSessionEnd(MavenSession session)
256 {
257 session.getAllProjects().forEach(this::createTmpFsBuildDir);
258 }
259 }