Repository: mesonbuild/meson

Issue:
java: support for linking jar files (using manifest)



Discussion:
Looking good. The only piece that is missing is removing the classpath entries when installing. This means updating `depfixer.py` to work on Jar files. It should extract the manifest file, remove all classpath entries (optionally add the `install_rpath`) and then store the manifest back in the Jar.
How should the `install_rpath` be added to the manifest? Can you give an example?
By manually editing manifest file so the line describing the classpath is changed to what you need. See an existing jar file's manifest to see how the runtime classpath entries are stored.
Ok, now that the classpath entry in the manifest is removed is this this RP acceptable? Travis doesn't pass because homebrew fails at fetching packages. (Nothing to do with these commits.)

I have also changed the default install path of jar files to share/java, which is used by all major Linux distributions. Should I move that to a separate RP?
> Should I move that to a separate RP?

Please do, adding new build options is always a bit touchy of a subject.
Done: https://github.com/mesonbuild/meson/pull/3830

Resolving patch:
diff --git a/mesonbuild/scripts/depfixer.py b/mesonbuild/scripts/depfixer.py
--- a/mesonbuild/scripts/depfixer.py
+++ b/mesonbuild/scripts/depfixer.py
@@ -457,7 +457,7 @@ def fix_darwin(fname: str, new_rpath: str, final_path: str, install_name_mapping
         raise SystemExit(err)
 
 def fix_jar(fname: str) -> None:
-    subprocess.check_call(['jar', 'xfv', fname, 'META-INF/MANIFEST.MF'])
+    subprocess.check_call(['jar', 'xf', fname, 'META-INF/MANIFEST.MF'])
     with open('META-INF/MANIFEST.MF', 'r+', encoding='utf-8') as f:
         lines = f.readlines()
         f.seek(0)
@@ -465,7 +465,12 @@ def fix_jar(fname: str) -> None:
             if not line.startswith('Class-Path:'):
                 f.write(line)
         f.truncate()
-    subprocess.check_call(['jar', 'ufm', fname, 'META-INF/MANIFEST.MF'])
+    # jar -um doesn't allow removing existing attributes.  Use -uM instead,
+    # which a) removes the existing manifest from the jar and b) disables
+    # special-casing for the manifest file, so we can re-add it as a normal
+    # archive member.  This puts the manifest at the end of the jar rather
+    # than the beginning, but the spec doesn't forbid that.
+    subprocess.check_call(['jar', 'ufM', fname, 'META-INF/MANIFEST.MF'])
 
 def fix_rpath(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Union[str, bytes], final_path: str, install_name_mappings: T.Dict[str, str], verbose: bool = True) -> None:
     global INSTALL_NAME_TOOL  # pylint: disable=global-statement
diff --git a/unittests/helpers.py b/unittests/helpers.py
--- a/unittests/helpers.py
+++ b/unittests/helpers.py
@@ -5,6 +5,7 @@
 import functools
 import re
 import typing as T
+import zipfile
 from pathlib import Path
 from contextlib import contextmanager
 
@@ -176,6 +177,22 @@ def get_rpath(fname: str) -> T.Optional[str]:
         return None
     return final
 
+def get_classpath(fname: str) -> T.Optional[str]:
+    with zipfile.ZipFile(fname) as zip:
+        with zip.open('META-INF/MANIFEST.MF') as member:
+            contents = member.read().decode().strip()
+    lines = []
+    for line in contents.splitlines():
+        if line.startswith(' '):
+            # continuation line
+            lines[-1] += line[1:]
+        else:
+            lines.append(line)
+    manifest = {
+        k.lower(): v.strip() for k, v in [l.split(':', 1) for l in lines]
+    }
+    return manifest.get('class-path')
+
 def get_path_without_cmd(cmd: str, path: str) -> str:
     pathsep = os.pathsep
     paths = OrderedSet([Path(p).resolve() for p in path.split(pathsep)])
diff --git a/unittests/machinefiletests.py b/unittests/machinefiletests.py
--- a/unittests/machinefiletests.py
+++ b/unittests/machinefiletests.py
@@ -42,6 +42,7 @@
 
 
 from run_tests import (
+    Backend,
     get_fake_env
 )
 
@@ -368,6 +369,23 @@ def test_java_compiler(self):
         self._single_implementation_compiler(
             'java', 'javac', 'javac 9.99.77', '9.99.77')
 
+    @skip_if_not_language('java')
+    def test_java_classpath(self):
+        if self.backend is not Backend.ninja:
+            raise SkipTest('Jar is only supported with Ninja')
+        testdir = os.path.join(self.unit_test_dir, '110 classpath')
+        self.init(testdir)
+        self.build()
+        one_build_path = get_classpath(os.path.join(self.builddir, 'one.jar'))
+        self.assertIsNone(one_build_path)
+        two_build_path = get_classpath(os.path.join(self.builddir, 'two.jar'))
+        self.assertEqual(two_build_path, 'one.jar')
+        self.install()
+        one_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/one.jar'))
+        self.assertIsNone(one_install_path)
+        two_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/two.jar'))
+        self.assertIsNone(two_install_path)
+
     @skip_if_not_language('swift')
     def test_swift_compiler(self):
         wrapper = self.helper_create_binary_wrapper(


Test patch:
diff --git a/test cases/unit/110 classpath/com/mesonbuild/Simple.java b/test cases/unit/110 classpath/com/mesonbuild/Simple.java
new file mode 100644
--- /dev/null
+++ b/test cases/unit/110 classpath/com/mesonbuild/Simple.java	
@@ -0,0 +1,7 @@
+package com.mesonbuild;
+
+class Simple {
+    public static void main(String [] args) {
+        System.out.println("Java is working.\n");
+    }
+}
diff --git a/test cases/unit/110 classpath/meson.build b/test cases/unit/110 classpath/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/110 classpath/meson.build	
@@ -0,0 +1,14 @@
+project('simplejava', 'java')
+
+one = jar('one', 'com/mesonbuild/Simple.java',
+  main_class : 'com.mesonbuild.Simple',
+  install : true,
+  install_dir : get_option('bindir'),
+)
+
+two = jar('two', 'com/mesonbuild/Simple.java',
+  main_class : 'com.mesonbuild.Simple',
+  install : true,
+  install_dir : get_option('bindir'),
+  link_with : one,
+)