8166568: Add jmod extract subcommand

8169492: jdk.internal.jmod.JmodFile.JMOD_MAGIC_NUMBER is a mutable array

Reviewed-by: alanb, anazarov, dfuchs, mchung
This commit is contained in:
Chris Hegarty 2016-12-10 14:19:53 +00:00
parent 37e807c698
commit 6d2dd6f483
6 changed files with 166 additions and 16 deletions

View File

@ -28,8 +28,10 @@ package jdk.internal.jmod;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@ -39,9 +41,9 @@ import java.util.zip.ZipFile;
*/
public class JmodFile implements AutoCloseable {
// jmod magic number and version number
public static final int JMOD_MAJOR_VERSION = 0x01;
public static final int JMOD_MINOR_VERSION = 0x00;
public static final byte[] JMOD_MAGIC_NUMBER = {
private static final int JMOD_MAJOR_VERSION = 0x01;
private static final int JMOD_MINOR_VERSION = 0x00;
private static final byte[] JMOD_MAGIC_NUMBER = {
0x4A, 0x4D, /* JM */
JMOD_MAJOR_VERSION, JMOD_MINOR_VERSION, /* version 1.0 */
};
@ -175,6 +177,10 @@ public class JmodFile implements AutoCloseable {
this.zipfile = new ZipFile(file.toFile());
}
public static void writeMagicNumber(OutputStream os) throws IOException {
os.write(JMOD_MAGIC_NUMBER);
}
/**
* Returns the {@code Entry} for a resource in a JMOD file section
* or {@code null} if not found.

View File

@ -36,6 +36,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import jdk.internal.jmod.JmodFile;
import static jdk.internal.jmod.JmodFile.*;
@ -57,7 +58,7 @@ class JmodOutputStream extends OutputStream implements AutoCloseable {
private JmodOutputStream(OutputStream out) {
this.zos = new ZipOutputStream(out);
try {
out.write(JMOD_MAGIC_NUMBER);
JmodFile.writeMagicNumber(out);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

View File

@ -138,6 +138,8 @@ public class JmodTask {
private static final String PROGNAME = "jmod";
private static final String MODULE_INFO = "module-info.class";
private static final Path CWD = Paths.get("");
private Options options;
private PrintWriter out = new PrintWriter(System.out, true);
void setLog(PrintWriter out, PrintWriter err) {
@ -153,6 +155,7 @@ public class JmodTask {
enum Mode {
CREATE,
EXTRACT,
LIST,
DESCRIBE,
HASH
@ -178,6 +181,7 @@ public class JmodTask {
Pattern modulesToHash;
boolean dryrun;
List<PathMatcher> excludes;
Path extractDir;
}
public int run(String[] args) {
@ -202,6 +206,9 @@ public class JmodTask {
case CREATE:
ok = create();
break;
case EXTRACT:
ok = extract();
break;
case LIST:
ok = list();
break;
@ -248,6 +255,32 @@ public class JmodTask {
}
}
private boolean extract() throws IOException {
Path dir = options.extractDir != null ? options.extractDir : CWD;
try (JmodFile jf = new JmodFile(options.jmodFile)) {
jf.stream().forEach(e -> {
try {
ZipEntry entry = e.zipEntry();
String name = entry.getName();
int index = name.lastIndexOf("/");
if (index != -1) {
Path p = dir.resolve(name.substring(0, index));
if (Files.notExists(p))
Files.createDirectories(p);
}
try (OutputStream os = Files.newOutputStream(dir.resolve(name))) {
jf.getInputStream(e).transferTo(os);
}
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
return true;
}
}
private boolean hashModules() {
return new Hasher(options.moduleFinder).run();
}
@ -1019,8 +1052,6 @@ public class JmodTask {
static class ClassPathConverter implements ValueConverter<Path> {
static final ValueConverter<Path> INSTANCE = new ClassPathConverter();
private static final Path CWD = Paths.get("");
@Override
public Path convert(String value) {
try {
@ -1044,8 +1075,6 @@ public class JmodTask {
static class DirPathConverter implements ValueConverter<Path> {
static final ValueConverter<Path> INSTANCE = new DirPathConverter();
private static final Path CWD = Paths.get("");
@Override
public Path convert(String value) {
try {
@ -1065,6 +1094,33 @@ public class JmodTask {
@Override public String valuePattern() { return "path"; }
}
static class ExtractDirPathConverter implements ValueConverter<Path> {
@Override
public Path convert(String value) {
try {
Path path = CWD.resolve(value);
if (Files.exists(path)) {
if (!Files.isDirectory(path))
throw new CommandException("err.cannot.create.dir", path);
} else {
try {
Files.createDirectories(path);
} catch (IOException ioe) {
throw new CommandException("err.cannot.create.dir", path);
}
}
return path;
} catch (InvalidPathException x) {
throw new CommandException("err.path.not.valid", value);
}
}
@Override public Class<Path> valueType() { return Path.class; }
@Override public String valuePattern() { return "path"; }
}
static class ModuleVersionConverter implements ValueConverter<Version> {
@Override
public Version convert(String value) {
@ -1158,6 +1214,7 @@ public class JmodTask {
builder.append(getMessage("main.opt.mode")).append("\n ");
builder.append(getMessage("main.opt.mode.create")).append("\n ");
builder.append(getMessage("main.opt.mode.extract")).append("\n ");
builder.append(getMessage("main.opt.mode.list")).append("\n ");
builder.append(getMessage("main.opt.mode.describe")).append("\n ");
builder.append(getMessage("main.opt.mode.hash")).append("\n\n");
@ -1203,6 +1260,11 @@ public class JmodTask {
.withValuesSeparatedBy(File.pathSeparatorChar)
.withValuesConvertedBy(DirPathConverter.INSTANCE);
OptionSpec<Path> dir
= parser.accepts("dir", getMessage("main.opt.extractDir"))
.withRequiredArg()
.withValuesConvertedBy(new ExtractDirPathConverter());
OptionSpec<Void> dryrun
= parser.accepts("dry-run", getMessage("main.opt.dry-run"));
@ -1303,6 +1365,8 @@ public class JmodTask {
options.cmds = opts.valuesOf(cmds);
if (opts.has(config))
options.configs = opts.valuesOf(config);
if (opts.has(dir))
options.extractDir = opts.valueOf(dir);
if (opts.has(dryrun))
options.dryrun = true;
if (opts.has(excludes))
@ -1347,7 +1411,8 @@ public class JmodTask {
if (options.mode.equals(Mode.CREATE) && Files.exists(path))
throw new CommandException("err.file.already.exists", path);
else if ((options.mode.equals(Mode.LIST) ||
options.mode.equals(Mode.DESCRIBE))
options.mode.equals(Mode.DESCRIBE) ||
options.mode.equals((Mode.EXTRACT)))
&& Files.notExists(path))
throw new CommandException("err.jmod.not.found", path);

View File

@ -24,11 +24,11 @@
#
main.usage.summary=\
Usage: {0} (create|list|describe|hash) <OPTIONS> <jmod-file>\n\
Usage: {0} (create|extract|list|describe|hash) <OPTIONS> <jmod-file>\n\
use --help for a list of possible options
main.usage=\
Usage: {0} (create|list|describe|hash) <OPTIONS> <jmod-file>\n\
Usage: {0} (create|extract|list|describe|hash) <OPTIONS> <jmod-file>\n\
error.prefix=Error:
warn.prefix=Warning:
@ -37,6 +37,8 @@ main.opt.mode=\
\Main operation modes:
main.opt.mode.create=\
\create - Creates a new jmod archive
main.opt.mode.extract=\
\extract - Extracts all the files from the archive
main.opt.mode.list=\
\list - Prints the names of all the entries
main.opt.mode.describe=\
@ -50,6 +52,7 @@ main.opt.class-path=Application jar files|dir containing classes
main.opt.libs=Location of native libraries
main.opt.cmds=Location of native commands
main.opt.config=Location of user-editable config files
main.opt.extractDir=Target directory for extract
main.opt.dry-run=Dry run of hash mode
main.opt.exclude=Exclude files matching the supplied comma separated pattern\
\ list, each element using one the following forms: <glob-pattern>,\
@ -75,8 +78,9 @@ main.opt.cmdfile=Read options from the specified file
module.hashes.recorded=Hashes are recorded in module {0}
err.missing.mode=one of create, list, describe, or hash must be specified
err.invalid.mode=mode must be one of create, list, describe, or hash: {0}
err.missing.mode=one of create, extract, list, describe, or hash must be specified
err.invalid.mode=mode must be one of create, extract, list, describe, or hash: {0}
err.cannot.create.dir=cannot create directory {0}
err.classpath.must.be.specified=--class-path must be specified
err.jmod.must.be.specified=jmod-file must be specified
err.invalid.version=invalid module version {0}

View File

@ -82,7 +82,7 @@ public class JmodNegativeTest {
jmod()
.assertFailure()
.resultChecker(r ->
assertContains(r.output, "Error: one of create, list, describe, or hash must be specified")
assertContains(r.output, "Error: one of create, extract, list, describe, or hash must be specified")
);
}
@ -91,7 +91,7 @@ public class JmodNegativeTest {
jmod("badAction")
.assertFailure()
.resultChecker(r ->
assertContains(r.output, "Error: mode must be one of create, list, describe, or hash")
assertContains(r.output, "Error: mode must be one of create, extract, list, describe, or hash")
);
jmod("--badOption")

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8142968
* @bug 8142968 8166568
* @summary Basic test for jmod
* @library /lib/testlibrary
* @modules jdk.compiler
@ -134,6 +134,70 @@ public class JmodTest {
});
}
@Test
public void testExtractCWD() throws IOException {
Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
jmod("create",
"--class-path", cp.toString(),
MODS_DIR.resolve("fooExtractCWD.jmod").toString())
.assertSuccess();
jmod("extract",
MODS_DIR.resolve("fooExtractCWD.jmod").toString())
.assertSuccess()
.resultChecker(r -> {
// module-info should exist, but jmod will have added its Packages attr.
assertTrue(Files.exists(Paths.get("classes/module-info.class")));
assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
Paths.get("classes/jdk/test/foo/Foo.class"));
assertSameContent(cp.resolve("jdk/test/foo/internal/Message.class"),
Paths.get("classes/jdk/test/foo/internal/Message.class"));
assertSameContent(cp.resolve("jdk/test/foo/resources/foo.properties"),
Paths.get("classes/jdk/test/foo/resources/foo.properties"));
});
}
@Test
public void testExtractDir() throws IOException {
if (Files.exists(Paths.get("extractTestDir")))
FileUtils.deleteFileTreeWithRetry(Paths.get("extractTestDir"));
Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
jmod("create",
"--conf", cf.toString(),
"--cmds", bp.toString(),
"--libs", lp.toString(),
"--class-path", cp.toString(),
MODS_DIR.resolve("fooExtractDir.jmod").toString())
.assertSuccess();
jmod("extract",
"--dir", "extractTestDir",
MODS_DIR.resolve("fooExtractDir.jmod").toString())
.assertSuccess();
jmod("extract",
"--dir", "extractTestDir",
MODS_DIR.resolve("fooExtractDir.jmod").toString())
.assertSuccess()
.resultChecker(r -> {
// check a sample of the extracted files
Path p = Paths.get("extractTestDir");
assertTrue(Files.exists(p.resolve("classes/module-info.class")));
assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
p.resolve("classes/jdk/test/foo/Foo.class"));
assertSameContent(bp.resolve("first"),
p.resolve(CMDS_PREFIX).resolve("first"));
assertSameContent(lp.resolve("first.so"),
p.resolve(LIBS_PREFIX).resolve("second.so"));
assertSameContent(cf.resolve("second.cfg"),
p.resolve(CONFIGS_PREFIX).resolve("second.cfg"));
});
}
@Test
public void testMainClass() throws IOException {
Path jmod = MODS_DIR.resolve("fooMainClass.jmod");
@ -532,6 +596,16 @@ public class JmodTest {
}
}
static void assertSameContent(Path p1, Path p2) {
try {
byte[] ba1 = Files.readAllBytes(p1);
byte[] ba2 = Files.readAllBytes(p2);
assertEquals(ba1, ba2);
} catch (IOException x) {
throw new UncheckedIOException(x);
}
}
static JmodResult jmod(String... args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);