Skip to content

Commit 725b710

Browse files
committed
Add embedded tar script packager
1 parent 51e9d3d commit 725b710

File tree

11 files changed

+643
-0
lines changed

11 files changed

+643
-0
lines changed

src/main/java/org/apache/netbeans/nbpackage/ArchiveUtils.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.io.IOException;
4242
import java.io.InputStream;
4343
import java.io.OutputStream;
44+
import java.nio.charset.Charset;
4445
import java.nio.charset.StandardCharsets;
4546
import java.nio.file.FileVisitResult;
4647
import java.nio.file.Files;
@@ -433,6 +434,48 @@ public static <E extends ArchiveEntry> void createArchive(ArchiveType archiveTyp
433434
}
434435
}
435436

437+
/**
438+
* Creates a tar file embedded in a shell script from the contents in {@code directoryToArchive}.
439+
*
440+
* @param directoryToArchive the directory to archive the contents of
441+
* @param archiveFile the file to write the archive to
442+
* @throws IOException if an IO error occurs
443+
* @throws ArchiveException if an archive error occurs
444+
*/
445+
public static void createEmbeddedTarScript(String shellScript, Path directoryToArchive, Path archiveFile)
446+
throws IOException, ArchiveException {
447+
try (OutputStream fileOutputStream = new BufferedOutputStream(Files.newOutputStream(archiveFile));
448+
TarArchiveOutputStream archiveOutputStream = new ArchiveStreamFactory()
449+
.createArchiveOutputStream(ArchiveType.TAR.getCommonsCompressName(), fileOutputStream)) {
450+
451+
fileOutputStream.write(shellScript.getBytes(Charset.forName("UTF-8")));
452+
453+
archiveOutputStream.setLongFileMode(LONGFILE_GNU);
454+
455+
456+
Files.walkFileTree(directoryToArchive, new SimpleFileVisitor<Path>() {
457+
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
458+
createAndPutArchiveEntry(ArchiveType.TAR, archiveOutputStream, directoryToArchive, file);
459+
archiveOutputStream.closeArchiveEntry();
460+
return FileVisitResult.CONTINUE;
461+
}
462+
463+
@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
464+
if (Files.isSameFile(dir, directoryToArchive)) {
465+
return FileVisitResult.CONTINUE;
466+
}
467+
468+
TarArchiveEntry entry = archiveOutputStream.createArchiveEntry(dir.toFile(), getRelativePathString(dir, directoryToArchive));
469+
archiveOutputStream.putArchiveEntry(entry);
470+
archiveOutputStream.closeArchiveEntry();
471+
return FileVisitResult.CONTINUE;
472+
}
473+
});
474+
475+
archiveOutputStream.finish();
476+
}
477+
}
478+
436479
@SuppressWarnings("unchecked")
437480
private static void createAndPutArchiveEntry(ArchiveType archiveType, ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream,
438481
Path directoryToArchive, Path filePathToArchive) throws IOException {

src/main/java/org/apache/netbeans/nbpackage/FileUtils.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@ public static void createZipArchive(Path directory, Path destination) throws IOE
107107
}
108108
}
109109

110+
/**
111+
* Create a tar file embedded in a shell script of the provided directory, maintaining file attributes.
112+
* The destination file must not already exist.
113+
*
114+
* @param directory directory to zip
115+
* @param destination destination file (must not exist)
116+
* @throws IOException
117+
*/
118+
public static void createEmbeddedTarScript(String script, Path directory, Path destination) throws IOException {
119+
if (Files.exists(destination)) {
120+
throw new IOException(destination.toString());
121+
}
122+
try {
123+
ArchiveUtils.createEmbeddedTarScript(script, directory, destination);
124+
} catch (ArchiveException ex) {
125+
throw new IOException(ex);
126+
}
127+
}
128+
110129
/**
111130
* Extract an archive into the given destination directory, maintaining file
112131
* attributes where possible. The destination directory must already exist.

src/main/java/org/apache/netbeans/nbpackage/NBPackage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.netbeans.nbpackage.innosetup.InnoSetupPackager;
3737
import org.apache.netbeans.nbpackage.macos.PkgPackager;
3838
import org.apache.netbeans.nbpackage.rpm.RpmPackager;
39+
import org.apache.netbeans.nbpackage.tar.TarScriptPackager;
3940
import org.apache.netbeans.nbpackage.zip.ZipPackager;
4041

4142
/**
@@ -130,6 +131,7 @@ public final class NBPackage {
130131
new AppImagePackager(),
131132
new DebPackager(),
132133
new RpmPackager(),
134+
new TarScriptPackager(),
133135
new InnoSetupPackager(),
134136
new PkgPackager(),
135137
new ZipPackager()
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.netbeans.nbpackage.tar;
20+
21+
import org.apache.netbeans.nbpackage.AbstractPackagerTask;
22+
import org.apache.netbeans.nbpackage.Architecture;
23+
import org.apache.netbeans.nbpackage.ExecutionContext;
24+
import org.apache.netbeans.nbpackage.FileUtils;
25+
import org.apache.netbeans.nbpackage.NBPackage;
26+
import org.apache.netbeans.nbpackage.StringUtils;
27+
import java.io.IOException;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.nio.file.attribute.PosixFilePermissions;
31+
import java.util.Locale;
32+
import java.util.Map;
33+
import java.util.Optional;
34+
import static org.apache.netbeans.nbpackage.Architecture.AARCH64;
35+
import static org.apache.netbeans.nbpackage.Architecture.X86_64;
36+
37+
class TarScriptPackageTask extends AbstractPackagerTask {
38+
39+
private static final String ARCH_AARCH64 = "aarch64";
40+
private static final String ARCH_X86_64 = "x86_64";
41+
private static final String ARCH_NOARCH = "noarch";
42+
43+
private String packageArch;
44+
45+
TarScriptPackageTask(ExecutionContext context) {
46+
super(context);
47+
}
48+
49+
@Override
50+
protected void customizeImage(Path image) throws Exception {
51+
String appDir = findLauncher(
52+
image.resolve("APPDIR").resolve("bin"))
53+
.getFileName().toString();
54+
55+
Path launcherDir = calculateAppPath(image).resolve("launcher");
56+
Files.createDirectories(launcherDir);
57+
58+
Path icon = context().getValue(TarScriptPackager.ICON_PATH).orElse(null);
59+
Path svg = context().getValue(TarScriptPackager.SVG_ICON_PATH).orElse(null);
60+
if (svg != null && icon == null) {
61+
context().warningHandler().accept(TarScriptPackager.MESSAGES.getString("message.svgnoicon"));
62+
svg = null;
63+
}
64+
65+
if (icon != null) {
66+
Files.copy(icon, launcherDir.resolve(appDir + ".png"));
67+
} else {
68+
Files.copy(getClass().getResourceAsStream(
69+
"/org/apache/netbeans/nbpackage/apache-netbeans-48x48.png"),
70+
launcherDir.resolve(appDir + ".png"));
71+
}
72+
if (svg != null) {
73+
Files.copy(svg, launcherDir.resolve(appDir + ".svg"));
74+
} else if (icon == null) {
75+
Files.copy(getClass().getResourceAsStream(
76+
"/org/apache/netbeans/nbpackage/apache-netbeans.svg"),
77+
launcherDir.resolve(appDir + ".svg"));
78+
}
79+
}
80+
81+
@Override
82+
protected Path buildPackage(Path image) throws Exception {
83+
String appDir = findLauncher(
84+
image.resolve("APPDIR").resolve("bin"))
85+
.getFileName().toString();
86+
87+
String appName = context().getValue(NBPackage.PACKAGE_NAME).orElse(appDir);
88+
String appNameSafe = sanitize(appName);
89+
90+
Path dst = context().destination().resolve(image.getFileName().toString() + ".sh");
91+
92+
String desktop = setupDesktopFile("exe", "icon");
93+
94+
String launcher = TarScriptPackager.LAUNCHER_TEMPLATE.load(context());
95+
96+
String template = TarScriptPackager.TAR_SCRIPT_TEMPLATE.load(context());
97+
Map<String, String> tokens = Map.of("package.tar.app_name_safe", appNameSafe,
98+
"package.tar.app_name", appName, "package.tar.app_dir", appDir,
99+
"package.tar.desktop", desktop, "package.tar.launcher", launcher);
100+
String script = StringUtils.replaceTokens(template,
101+
key -> {
102+
var ret = tokens.get(key);
103+
if (ret != null) {
104+
return ret;
105+
} else {
106+
return context().tokenReplacementFor(key);
107+
}
108+
});
109+
110+
FileUtils.createEmbeddedTarScript(script, image, dst);
111+
112+
try {
113+
Files.setPosixFilePermissions(dst, PosixFilePermissions.fromString("rwxr-xr-x"));
114+
} catch (UnsupportedOperationException ex) {
115+
context().warningHandler().accept("UnsupportedOperationException : PosixFilePermissions");
116+
}
117+
return dst;
118+
}
119+
120+
@Override
121+
protected String calculateImageName(Path input) throws Exception {
122+
return super.calculateImageName(input) + "." + packageArch();
123+
}
124+
125+
@Override
126+
protected Path calculateRuntimePath(Path image, Path application) throws Exception {
127+
return application.resolve("jdk");
128+
}
129+
130+
@Override
131+
protected Path calculateAppPath(Path image) throws IOException {
132+
return image.resolve("APPDIR");
133+
}
134+
135+
136+
private String packageArch() {
137+
if (packageArch == null) {
138+
packageArch = context().getValue(NBPackage.PACKAGE_ARCH)
139+
.orElseGet(() -> {
140+
Optional<Path> runtime = context().getValue(NBPackage.PACKAGE_RUNTIME);
141+
if (runtime.isPresent()) {
142+
return Architecture.detectFromPath(
143+
runtime.get()).map(a -> {
144+
return switch (a) {
145+
case AARCH64 ->
146+
ARCH_AARCH64;
147+
case X86_64 ->
148+
ARCH_X86_64;
149+
};
150+
}).orElseGet(() -> {
151+
context().warningHandler().accept(
152+
TarScriptPackager.MESSAGES.getString("message.unknownarch"));
153+
return ARCH_NOARCH;
154+
});
155+
} else {
156+
return ARCH_NOARCH;
157+
}
158+
});
159+
}
160+
return packageArch;
161+
}
162+
163+
private String sanitize(String text) {
164+
return text.toLowerCase(Locale.ROOT)
165+
.replaceAll("[^a-z0-9\\+\\-\\.]", "-");
166+
}
167+
168+
private Path findLauncher(Path binDir) throws IOException {
169+
try ( var files = Files.list(binDir)) {
170+
return files.filter(f -> !f.getFileName().toString().endsWith(".exe"))
171+
.findFirst().orElseThrow(IOException::new);
172+
}
173+
}
174+
175+
private String setupDesktopFile(String exec, String pkgName) throws IOException {
176+
String template = TarScriptPackager.DESKTOP_TEMPLATE.load(context());
177+
return context().replaceTokens(template);
178+
}
179+
180+
}

0 commit comments

Comments
 (0)