Packaging CDS archive file (.jsa) in deploy JAR.

This is the first step of a series of planned changes to support producing a CDS archive at build-time automatically and embedding it into the output deploy JAR as part of the 'java_binary' rule action, when it is opt-in.

This CL augments the singlejar tool to prepend the CDS archive data before the JAR content but after the launcher binary, if '--cds_archive <archive_path>' is specified. The start of the CDS archive within the deploy JAR is aligned at page boundary, which is required by file-backed mapping. The start offset is recorded as an attribute, 'Jsa-Offset' in the deploy JAR META-INF/MANIFEST.MF. The recorded 'Jsa-Offset' is used by the JVM to locate and map the archived data at runtime.

RELNOTES: Add --cds_archive option for embedding CDS archive into deploy JAR.
PiperOrigin-RevId: 295991052
diff --git a/src/tools/singlejar/options.cc b/src/tools/singlejar/options.cc
index 95189c9..f6673c7 100644
--- a/src/tools/singlejar/options.cc
+++ b/src/tools/singlejar/options.cc
@@ -36,6 +36,7 @@
   if (tokens->MatchAndSet("--output", &output_jar) ||
       tokens->MatchAndSet("--main_class", &main_class) ||
       tokens->MatchAndSet("--java_launcher", &java_launcher) ||
+      tokens->MatchAndSet("--cds_archive", &cds_archive) ||
       tokens->MatchAndSet("--deploy_manifest_lines", &manifest_lines) ||
       tokens->MatchAndSet("--sources", &input_jars) ||
       tokens->MatchAndSet("--resources", &resources) ||
diff --git a/src/tools/singlejar/options.h b/src/tools/singlejar/options.h
index 1125dd6..0c3c960 100644
--- a/src/tools/singlejar/options.h
+++ b/src/tools/singlejar/options.h
@@ -43,6 +43,7 @@
   std::string output_jar;
   std::string main_class;
   std::string java_launcher;
+  std::string cds_archive;
   std::vector<std::string> manifest_lines;
   std::vector<std::pair<std::string, std::string> > input_jars;
   std::vector<std::string> resources;
diff --git a/src/tools/singlejar/options_test.cc b/src/tools/singlejar/options_test.cc
index 90a9b19..e9e591b 100644
--- a/src/tools/singlejar/options_test.cc
+++ b/src/tools/singlejar/options_test.cc
@@ -65,7 +65,8 @@
                         "--build_info_file", "build_file1",
                         "--extra_build_info", "extra_build_line1",
                         "--build_info_file", "build_file2",
-                        "--extra_build_info", "extra_build_line2"};
+                        "--extra_build_info", "extra_build_line2",
+                        "--cds_archive", "classes.jsa"};
   Options options;
   options.ParseCommandLine(arraysize(args), args);
 
@@ -78,6 +79,7 @@
   ASSERT_EQ(2UL, options.build_info_lines.size());
   EXPECT_EQ("extra_build_line1", options.build_info_lines[0]);
   EXPECT_EQ("extra_build_line2", options.build_info_lines[1]);
+  EXPECT_EQ("classes.jsa", options.cds_archive);
 }
 
 TEST(OptionsTest, MultiOptargs) {
diff --git a/src/tools/singlejar/output_jar.cc b/src/tools/singlejar/output_jar.cc
index 87ce0d8..7c38c74 100644
--- a/src/tools/singlejar/output_jar.cc
+++ b/src/tools/singlejar/output_jar.cc
@@ -122,28 +122,7 @@
 
   // Copy launcher if it is set.
   if (!options_->java_launcher.empty()) {
-    const char *const launcher_path = options_->java_launcher.c_str();
-    int in_fd = open(launcher_path, O_RDONLY);
-    struct stat statbuf;
-    if (file_ == nullptr || fstat(in_fd, &statbuf)) {
-      diag_err(1, "%s", launcher_path);
-    }
-    // TODO(asmundak):  Consider going back to sendfile() or reflink
-    // (BTRFS_IOC_CLONE/XFS_IOC_CLONE) here.  The launcher preamble can
-    // be very large for targets with many native deps.
-    ssize_t byte_count = AppendFile(in_fd, 0, statbuf.st_size);
-    if (byte_count < 0) {
-      diag_err(1, "%s:%d: Cannot copy %s to %s", __FILE__, __LINE__,
-               launcher_path, options_->output_jar.c_str());
-    } else if (byte_count != statbuf.st_size) {
-      diag_err(1, "%s:%d: Copied only %zu bytes out of %" PRIu64 " from %s",
-               __FILE__, __LINE__, byte_count, statbuf.st_size, launcher_path);
-    }
-    close(in_fd);
-    if (options_->verbose) {
-      fprintf(stderr, "Prepended %s (%" PRIu64 " bytes)\n", launcher_path,
-              statbuf.st_size);
-    }
+    AppendFile(options_, options_->java_launcher.c_str());
   }
 
   if (!options_->main_class.empty()) {
@@ -153,6 +132,11 @@
     manifest_.Append("\r\n");
   }
 
+  // Copy CDS archive file (.jsa) if it is set.
+  if (!options_->cds_archive.empty()) {
+    AppendCDSArchive(options->cds_archive);
+  }
+
   for (auto &manifest_line : options_->manifest_lines) {
     if (!manifest_line.empty()) {
       manifest_.Append(manifest_line);
@@ -959,7 +943,7 @@
   }
 }
 
-ssize_t OutputJar::AppendFile(int in_fd, off64_t offset, size_t count) {
+ssize_t OutputJar::CopyAppendData(int in_fd, off64_t offset, size_t count) {
   if (count == 0) {
     return 0;
   }
@@ -1005,6 +989,66 @@
   return total_written;
 }
 
+void OutputJar::AppendFile(Options *options, const char *const file_path) {
+  int in_fd = open(file_path, O_RDONLY);
+  struct stat statbuf;
+  if (fstat(in_fd, &statbuf)) {
+    diag_err(1, "%s", file_path);
+  }
+  // TODO(asmundak):  Consider going back to sendfile() or reflink
+  // (BTRFS_IOC_CLONE/XFS_IOC_CLONE) here.  The launcher preamble can
+  // be very large for targets with many native deps.
+  ssize_t byte_count = CopyAppendData(in_fd, 0, statbuf.st_size);
+  if (byte_count < 0) {
+    diag_err(1, "%s:%d: Cannot copy %s to %s", __FILE__, __LINE__,
+             file_path, options->output_jar.c_str());
+  } else if (byte_count != statbuf.st_size) {
+    diag_err(1, "%s:%d: Copied only %zu bytes out of %" PRIu64 " from %s",
+             __FILE__, __LINE__, byte_count, statbuf.st_size, file_path);
+  }
+  close(in_fd);
+  if (options->verbose) {
+    fprintf(stderr, "Prepended %s (%" PRIu64 " bytes)\n", file_path,
+            statbuf.st_size);
+  }
+}
+
+void OutputJar::AppendCDSArchive(const std::string &cds_archive) {
+  // Align the shared archive start offset at page alignment, which is
+  // required by mmap.
+  off64_t cur_offset = Position();
+  size_t pagesize;
+#ifdef _WIN32
+  SYSTEM_INFO si;
+  GetSystemInfo(&si);
+  pagesize = si.dwPageSize;
+#else
+  pagesize = sysconf(_SC_PAGESIZE);
+#endif
+  off64_t aligned_offset = (cur_offset + (pagesize - 1)) & ~(pagesize - 1);
+  size_t gap = aligned_offset - cur_offset;
+  if (gap > 0) {
+    char zero = 0;
+    size_t written = fwrite(&zero, 1, gap, file_);
+    outpos_ += written;
+  }
+
+  // Copy archived data
+  AppendFile(options_, cds_archive.c_str());
+
+  // Write the file offset of the shared archive section as a manifest
+  // attribute.
+  char cds_manifest_attr[50];
+  snprintf(cds_manifest_attr, sizeof(cds_manifest_attr),
+           "Jsa-Offset: %ld", aligned_offset);
+  manifest_.Append(cds_manifest_attr);
+  manifest_.Append("\r\n");
+
+  // Add to build_properties
+  build_properties_.AddProperty("cds.archive",
+                                cds_archive.c_str());
+}
+
 void OutputJar::ExtraCombiner(const std::string &entry_name,
                               Combiner *combiner) {
   extra_combiners_.emplace_back(combiner);
diff --git a/src/tools/singlejar/output_jar.h b/src/tools/singlejar/output_jar.h
index 18ecb10..322aca3 100644
--- a/src/tools/singlejar/output_jar.h
+++ b/src/tools/singlejar/output_jar.h
@@ -96,8 +96,12 @@
   // Set classpath resource with given resource name and path.
   void ClasspathResource(const std::string& resource_name,
                          const std::string& resource_path);
+  // Append CDS archive file.
+  void AppendCDSArchive(const std::string &cds_archive);
+  // Append data from the file specified by file_path.
+  void AppendFile(Options *options, const char *const file_path);
   // Copy 'count' bytes starting at 'offset' from the given file.
-  ssize_t AppendFile(int in_fd, off64_t offset, size_t count);
+  ssize_t CopyAppendData(int in_fd, off64_t offset, size_t count);
   // Write bytes to the output file, return true on success.
   bool WriteBytes(const void *buffer, size_t count);
 
diff --git a/src/tools/singlejar/output_jar_simple_test.cc b/src/tools/singlejar/output_jar_simple_test.cc
index 349439a..7b427c8 100644
--- a/src/tools/singlejar/output_jar_simple_test.cc
+++ b/src/tools/singlejar/output_jar_simple_test.cc
@@ -277,6 +277,36 @@
   input_jar.Close();
 }
 
+// --cds_archive option
+TEST_F(OutputJarSimpleTest, CDSArchive) {
+  string out_path = OutputFilePath("out.jar");
+  string launcher_path = CreateTextFile("launcher", "Dummy");
+  string cds_archive_path = CreateTextFile("classes.jsa", "Dummy");
+  CreateOutput(out_path, {"--java_launcher", launcher_path,
+                          "--cds_archive", cds_archive_path});
+
+  // check META-INF/MANIFEST.MF attribute
+  string manifest = GetEntryContents(out_path, "META-INF/MANIFEST.MF");
+  size_t pagesize;
+#ifndef _WIN32
+  pagesize = sysconf(_SC_PAGESIZE);
+#else
+  SYSTEM_INFO si;
+  GetSystemInfo(&si);
+  pagesize = si.dwPageSize;
+#endif
+  char attr[128];
+  snprintf(attr, sizeof(attr), "Jsa-Offset: %ld", pagesize);
+  EXPECT_PRED2(HasSubstr, manifest, attr);
+
+  // check build-data.properties entry
+  string build_properties = GetEntryContents(out_path, "build-data.properties");
+  char prop[4096];
+  snprintf(prop, sizeof(prop), "\ncds.archive=%s\n",
+           cds_archive_path.c_str());
+  EXPECT_PRED2(HasSubstr, build_properties, prop);
+}
+
 // --main_class option.
 TEST_F(OutputJarSimpleTest, MainClass) {
   string out_path = OutputFilePath("out.jar");