Skip to content

Commit 146605c

Browse files
committed
Add embedded tar script packager
1 parent a4ccf75 commit 146605c

11 files changed

Lines changed: 654 additions & 3 deletions

File tree

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

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
3434
import org.apache.commons.compress.archivers.zip.ZipFile;
3535
import org.apache.commons.compress.compressors.CompressorException;
36+
import org.apache.commons.compress.compressors.CompressorOutputStream;
3637
import org.apache.commons.compress.compressors.CompressorStreamFactory;
3738
import org.apache.commons.compress.utils.IOUtils;
3839

@@ -41,12 +42,14 @@
4142
import java.io.IOException;
4243
import java.io.InputStream;
4344
import java.io.OutputStream;
45+
import java.nio.charset.Charset;
4446
import java.nio.charset.StandardCharsets;
4547
import java.nio.file.FileVisitResult;
4648
import java.nio.file.Files;
4749
import java.nio.file.Path;
4850
import java.nio.file.SimpleFileVisitor;
4951
import java.nio.file.StandardCopyOption;
52+
import java.nio.file.StandardOpenOption;
5053
import java.nio.file.attribute.BasicFileAttributeView;
5154
import java.nio.file.attribute.BasicFileAttributes;
5255
import java.nio.file.attribute.FileTime;
@@ -77,7 +80,7 @@
7780
public static final int OTHERS_READ_BIT_MASK = 00004;
7881
public static final int OTHERS_WRITE_BIT_MASK = 00002;
7982
public static final int OTHERS_EXECUTE_BIT_MASK = 00001;
80-
83+
8184
private static final System.Logger LOG = System.getLogger(ArchiveUtils.class.getName());
8285

8386
/**
@@ -433,6 +436,53 @@ public static <E extends ArchiveEntry> void createArchive(ArchiveType archiveTyp
433436
}
434437
}
435438

439+
/**
440+
* Creates a gzip tar file embedded in a shell script from the contents in {@code directoryToArchive}.
441+
*
442+
* @param directoryToArchive the directory to archive the contents of
443+
* @param archiveFile the file to write the archive to
444+
* @throws IOException if an IO error occurs
445+
* @throws ArchiveException if an archive error occurs
446+
*/
447+
public static void createEmbeddedGzipTarScript(String shellScript, Path directoryToArchive, Path archiveFile)
448+
throws IOException, ArchiveException {
449+
450+
try (OutputStream fileOutputStream = new BufferedOutputStream(Files.newOutputStream(archiveFile))) {
451+
fileOutputStream.write(shellScript.getBytes(Charset.forName("UTF-8")));
452+
}
453+
454+
try (OutputStream fileOutputStream = new BufferedOutputStream(Files.newOutputStream(archiveFile, StandardOpenOption.APPEND));
455+
CompressorOutputStream<?> gzipOutputStream =
456+
CompressorStreamFactory.getSingleton().createCompressorOutputStream(CompressorStreamFactory.GZIP, fileOutputStream);
457+
TarArchiveOutputStream archiveOutputStream = new ArchiveStreamFactory()
458+
.createArchiveOutputStream(ArchiveStreamFactory.TAR, gzipOutputStream)) {
459+
460+
archiveOutputStream.setLongFileMode(LONGFILE_GNU);
461+
462+
463+
Files.walkFileTree(directoryToArchive, new SimpleFileVisitor<Path>() {
464+
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
465+
createAndPutArchiveEntry(ArchiveType.TAR, archiveOutputStream, directoryToArchive, file);
466+
archiveOutputStream.closeArchiveEntry();
467+
return FileVisitResult.CONTINUE;
468+
}
469+
470+
@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
471+
if (Files.isSameFile(dir, directoryToArchive)) {
472+
return FileVisitResult.CONTINUE;
473+
}
474+
475+
TarArchiveEntry entry = archiveOutputStream.createArchiveEntry(dir.toFile(), getRelativePathString(dir, directoryToArchive));
476+
archiveOutputStream.putArchiveEntry(entry);
477+
archiveOutputStream.closeArchiveEntry();
478+
return FileVisitResult.CONTINUE;
479+
}
480+
});
481+
482+
archiveOutputStream.finish();
483+
}
484+
}
485+
436486
@SuppressWarnings("unchecked")
437487
private static void createAndPutArchiveEntry(ArchiveType archiveType, ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream,
438488
Path directoryToArchive, Path filePathToArchive) throws IOException {
@@ -459,7 +509,7 @@ private static void createAndPutArchiveEntry(ArchiveType archiveType, ArchiveOut
459509
if (isSymbolicLink) {
460510
entry = new TarArchiveEntry(getRelativePathString(filePathToArchive, directoryToArchive), TarConstants.LF_SYMLINK);
461511
entry.setLinkName(getRelativePathString(
462-
filePathToArchive.resolve(Files.readSymbolicLink(filePathToArchive)),
512+
filePathToArchive.resolve(Files.readSymbolicLink(filePathToArchive)),
463513
directoryToArchive));
464514
} else {
465515
entry = new TarArchiveEntry(filePathToArchive.toFile(), getRelativePathString(filePathToArchive, directoryToArchive));

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ public static void createZipArchive(Path directory, Path destination) throws IOE
107107
}
108108
}
109109

110+
/**
111+
* Create a gzip 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 script the shell script
115+
* @param directory directory to zip
116+
* @param destination destination file (must not exist)
117+
* @throws IOException
118+
*/
119+
public static void createEmbeddedGzipTarScript(String script, Path directory, Path destination) throws IOException {
120+
if (Files.exists(destination)) {
121+
throw new IOException(destination.toString());
122+
}
123+
try {
124+
ArchiveUtils.createEmbeddedGzipTarScript(script, directory, destination);
125+
} catch (ArchiveException ex) {
126+
throw new IOException(ex);
127+
}
128+
}
129+
110130
/**
111131
* Extract an archive into the given destination directory, maintaining file
112132
* attributes where possible. The destination directory must already exist.
@@ -217,7 +237,7 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx
217237
public static List<Path> find(Path searchDir, String pattern) throws IOException {
218238
var matcher = searchDir.getFileSystem().getPathMatcher("glob:" + pattern);
219239
try (var stream = Files.find(searchDir, Integer.MAX_VALUE, (file, attr)
220-
-> !file.equals(searchDir) &&
240+
-> !file.equals(searchDir) &&
221241
(matcher.matches(searchDir.relativize(file)) ||
222242
matcher.matches(file.getFileName()))
223243
)) {

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.createEmbeddedGzipTarScript(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)