# HG changeset patch # User chegar # Date 1481201845 0 # Thu Dec 08 12:57:25 2016 +0000 # Node ID 661958959f3516b4ad227d9273f2b49e2fb569fd # Parent c1b4d9879aea8313b3746e295c5bfce8818b3cd6 8166568: Add jmod extract subcommand 8169492: jdk.internal.jmod.JmodFile.JMOD_MAGIC_NUMBER is a mutable array Reviewed-by: alanb, dfuchs, mchung diff --git a/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java b/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java --- a/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java +++ b/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java @@ -28,8 +28,10 @@ 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 @@ */ 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 @@ 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. diff --git a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java --- a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java @@ -36,6 +36,7 @@ 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 @@ 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); } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java --- a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java @@ -138,6 +138,8 @@ 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 @@ enum Mode { CREATE, + EXTRACT, LIST, DESCRIBE, HASH @@ -178,6 +181,7 @@ Pattern modulesToHash; boolean dryrun; List excludes; + Path extractDir; } public int run(String[] args) { @@ -202,6 +206,9 @@ case CREATE: ok = create(); break; + case EXTRACT: + ok = extract(); + break; case LIST: ok = list(); break; @@ -248,6 +255,32 @@ } } + 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 @@ static class ClassPathConverter implements ValueConverter { static final ValueConverter INSTANCE = new ClassPathConverter(); - private static final Path CWD = Paths.get(""); - @Override public Path convert(String value) { try { @@ -1044,8 +1075,6 @@ static class DirPathConverter implements ValueConverter { static final ValueConverter INSTANCE = new DirPathConverter(); - private static final Path CWD = Paths.get(""); - @Override public Path convert(String value) { try { @@ -1065,6 +1094,33 @@ @Override public String valuePattern() { return "path"; } } + static class ExtractDirPathConverter implements ValueConverter { + + @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 valueType() { return Path.class; } + + @Override public String valuePattern() { return "path"; } + } + static class ModuleVersionConverter implements ValueConverter { @Override public Version convert(String value) { @@ -1158,6 +1214,7 @@ 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 @@ .withValuesSeparatedBy(File.pathSeparatorChar) .withValuesConvertedBy(DirPathConverter.INSTANCE); + OptionSpec dir + = parser.accepts("dir", getMessage("main.opt.extractDir")) + .withRequiredArg() + .withValuesConvertedBy(new ExtractDirPathConverter()); + OptionSpec dryrun = parser.accepts("dry-run", getMessage("main.opt.dry-run")); @@ -1303,6 +1365,8 @@ 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 @@ 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); diff --git a/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties b/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties --- a/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties +++ b/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties @@ -24,11 +24,11 @@ # main.usage.summary=\ -Usage: {0} (create|list|describe|hash) \n\ +Usage: {0} (create|extract|list|describe|hash) \n\ use --help for a list of possible options main.usage=\ -Usage: {0} (create|list|describe|hash) \n\ +Usage: {0} (create|extract|list|describe|hash) \n\ error.prefix=Error: warn.prefix=Warning: @@ -37,6 +37,8 @@ \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.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: ,\ @@ -75,8 +78,9 @@ 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} diff --git a/test/tools/jmod/JmodNegativeTest.java b/test/tools/jmod/JmodNegativeTest.java --- a/test/tools/jmod/JmodNegativeTest.java +++ b/test/tools/jmod/JmodNegativeTest.java @@ -82,7 +82,7 @@ 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 @@ 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") diff --git a/test/tools/jmod/JmodTest.java b/test/tools/jmod/JmodTest.java --- a/test/tools/jmod/JmodTest.java +++ b/test/tools/jmod/JmodTest.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8142968 + * @bug 8142968 8166568 * @summary Basic test for jmod * @library /lib/testlibrary * @modules jdk.compiler @@ -135,6 +135,65 @@ } @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() + .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"); FileUtils.deleteFileIfExistsWithRetry(jmod); @@ -532,6 +591,16 @@ } } + 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);