This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new d2fe5696f chore(release): enforce OpenJDK 25 for JVM publishing (#3775)
d2fe5696f is described below

commit d2fe5696f4d82575f6bdbfad905bb78a77c5118c
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jun 21 14:59:56 2026 +0530

    chore(release): enforce OpenJDK 25 for JVM publishing (#3775)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    Closes #3771
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 ci/release.py                                      | 301 +++++++++++++++++++++
 .../java/org/apache/fory/logging/ForyLogger.java   |   2 +-
 java/pom.xml                                       |  30 ++
 kotlin/pom.xml                                     |  33 +++
 4 files changed, 365 insertions(+), 1 deletion(-)

diff --git a/ci/release.py b/ci/release.py
index 9ebea5672..279bdcd33 100644
--- a/ci/release.py
+++ b/ci/release.py
@@ -22,11 +22,30 @@ import os
 import re
 import shutil
 import subprocess
+import sys
+import xml.etree.ElementTree as ET
+import zipfile
 
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
 PROJECT_ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 
"../")
+JVM_RELEASE_LANGS = ("java", "kotlin", "scala")
+HOMEBREW_OPENJDK25 = "openjdk@25"
+HOMEBREW_BREW_PATHS = ("/opt/homebrew/bin/brew", "/usr/local/bin/brew")
+FORY_CORE_JDK25_ENTRY = (
+    "META-INF/versions/25/org/apache/fory/reflect/InstanceFieldAccessors.class"
+)
+FORY_CORE_ACCESSOR = 
"org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor"
+MAVEN_RELEASE_CMD = (
+    "mvn -T10 clean deploy --no-transfer-progress -DskipTests -Papache-release"
+)
+SCALA_RELEASE_CMDS = (
+    "sbt clean",
+    "sbt +publishSigned",
+    "sbt sonatypePrepare",
+    "sbt sonatypeBundleUpload",
+)
 
 
 def prepare(v: str):
@@ -134,6 +153,257 @@ def verify(v):
     logger.info("Verified checksum successfully")
 
 
+def publish_jvm(languages="all"):
+    """Publish Java, Kotlin, and Scala artifacts."""
+    langs = _jvm_release_langs(languages)
+    _ensure_openjdk25()
+    for lang in langs:
+        if lang == "java":
+            _publish_java()
+            _verify_fory_core_mr_jar()
+        elif lang == "kotlin":
+            _publish_kotlin()
+        elif lang == "scala":
+            _publish_scala()
+        else:
+            raise NotImplementedError(f"Unsupported JVM release language: 
{lang}")
+    _verify_fory_core_mr_jar()
+
+
+def publish_java():
+    publish_jvm("java")
+
+
+def publish_kotlin():
+    publish_jvm("kotlin")
+
+
+def publish_scala():
+    publish_jvm("scala")
+
+
+def _jvm_release_langs(languages):
+    if languages in (None, "", "all"):
+        return list(JVM_RELEASE_LANGS)
+    langs = [lang.strip() for lang in languages.split(",") if lang.strip()]
+    unsupported = [lang for lang in langs if lang not in JVM_RELEASE_LANGS]
+    if unsupported:
+        raise ValueError(f"Unsupported JVM release language(s): {unsupported}")
+    return langs
+
+
+def _publish_java():
+    _run_release_cmd(MAVEN_RELEASE_CMD, "java")
+
+
+def _publish_kotlin():
+    _run_release_cmd(MAVEN_RELEASE_CMD, "kotlin")
+
+
+def _publish_scala():
+    for command in SCALA_RELEASE_CMDS:
+        _run_release_cmd(command, "scala")
+
+
+def _run_release_cmd(command, path):
+    cwd = os.path.join(PROJECT_ROOT_DIR, path)
+    logger.info("Run release command in %s: %s", cwd, command)
+    subprocess.check_call(command, cwd=cwd, shell=True)
+
+
+def _ensure_openjdk25():
+    runtime = _read_java_runtime(_java_tool("java"))
+    # The JDK25 multi-release Maven profile is JVM-activated; a lower release
+    # JDK silently publishes a jar without the required JDK25 overlay.
+    if runtime and _is_openjdk25(runtime):
+        _export_java_home(runtime["props"]["java.home"])
+        logger.info("Using OpenJDK 25 release runtime: %s", 
os.environ["JAVA_HOME"])
+        return
+    if sys.platform != "darwin":
+        raise RuntimeError(
+            "JVM releases must run with OpenJDK 25. "
+            "Install OpenJDK 25 and set JAVA_HOME/PATH before running 
release.py. "
+            f"Found {_java_runtime_summary(runtime)}."
+        )
+    java_home = _homebrew_openjdk25_home()
+    _export_java_home(java_home)
+    runtime = _read_java_runtime(_java_tool("java"))
+    if not runtime or not _is_openjdk25(runtime):
+        raise RuntimeError(
+            "JVM releases must run with OpenJDK 25. "
+            f"Found {_java_runtime_summary(runtime)} after setting "
+            f"JAVA_HOME={java_home}."
+        )
+    logger.info("Using OpenJDK 25 release runtime: %s", 
os.environ["JAVA_HOME"])
+
+
+def _homebrew_openjdk25_home():
+    brew = _brew_command()
+    if not brew:
+        raise RuntimeError(
+            "Cannot install OpenJDK 25 automatically because Homebrew was not 
found."
+        )
+    prefix = _homebrew_prefix(brew, HOMEBREW_OPENJDK25)
+    if not prefix:
+        logger.info("Installing %s with Homebrew", HOMEBREW_OPENJDK25)
+        subprocess.check_call([brew, "install", HOMEBREW_OPENJDK25])
+        prefix = _homebrew_prefix(brew, HOMEBREW_OPENJDK25)
+    if not prefix:
+        raise RuntimeError(f"Cannot locate Homebrew formula 
{HOMEBREW_OPENJDK25}")
+    for candidate in [
+        os.path.join(prefix, "libexec", "openjdk.jdk", "Contents", "Home"),
+        prefix,
+    ]:
+        if os.path.exists(os.path.join(candidate, "bin", "java")):
+            return candidate
+    raise RuntimeError(f"Cannot find a java executable under {prefix}")
+
+
+def _brew_command():
+    brew = shutil.which("brew")
+    if brew:
+        return brew
+    for brew in HOMEBREW_BREW_PATHS:
+        if os.path.exists(brew):
+            return brew
+    return None
+
+
+def _homebrew_prefix(brew, formula):
+    proc = subprocess.run(
+        [brew, "--prefix", formula],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        universal_newlines=True,
+    )
+    if proc.returncode != 0:
+        return None
+    return proc.stdout.strip()
+
+
+def _export_java_home(java_home):
+    os.environ["JAVA_HOME"] = java_home
+    java_bin = os.path.join(java_home, "bin")
+    path_entries = [
+        entry for entry in os.environ.get("PATH", "").split(os.pathsep) if 
entry
+    ]
+    path_entries = [entry for entry in path_entries if entry != java_bin]
+    os.environ["PATH"] = os.pathsep.join([java_bin] + path_entries)
+
+
+def _read_java_runtime(java_cmd):
+    try:
+        proc = subprocess.run(
+            [java_cmd, "-XshowSettings:properties", "-version"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+            check=True,
+        )
+    except (OSError, subprocess.CalledProcessError):
+        return None
+    return {"props": _java_props(proc.stdout), "output": proc.stdout}
+
+
+def _is_openjdk25(runtime):
+    props = runtime["props"]
+    spec_version = props.get("java.specification.version", "")
+    runtime_name = props.get("java.runtime.name", "")
+    vm_name = props.get("java.vm.name", "")
+    is_openjdk = "openjdk" in f"{runtime_name} {vm_name} 
{runtime['output']}".lower()
+    return spec_version == "25" and is_openjdk
+
+
+def _java_runtime_summary(runtime):
+    if not runtime:
+        return "no Java runtime"
+    props = runtime["props"]
+    return (
+        f"java.home={props.get('java.home', '')}, "
+        f"java.version={props.get('java.version', '')}, "
+        f"java.specification.version={props.get('java.specification.version', 
'')}, "
+        f"java.runtime.name={props.get('java.runtime.name', '')}, "
+        f"java.vm.name={props.get('java.vm.name', '')}"
+    )
+
+
+def _java_tool(tool):
+    java_home = os.environ.get("JAVA_HOME")
+    if java_home:
+        return os.path.join(java_home, "bin", tool)
+    return tool
+
+
+def _java_props(output):
+    props = {}
+    for line in output.splitlines():
+        match = re.match(r"\s*([^=]+?)\s*=\s*(.*)\s*$", line)
+        if match:
+            props[match.group(1)] = match.group(2)
+    return props
+
+
+def _verify_fory_core_mr_jar():
+    jar_path = _fory_core_jar_path()
+    if not os.path.exists(jar_path):
+        raise FileNotFoundError(
+            f"Missing fory-core release jar: {jar_path}. "
+            "Run the Java release before publishing Kotlin or Scala artifacts."
+        )
+    with zipfile.ZipFile(jar_path) as jar:
+        names = set(jar.namelist())
+        manifest = jar.read("META-INF/MANIFEST.MF").decode("utf-8")
+    if "Multi-Release: true" not in manifest:
+        raise RuntimeError(f"{jar_path} is missing manifest Multi-Release: 
true")
+    if "Build-Jdk-Spec: 25" not in manifest:
+        raise RuntimeError(f"{jar_path} was not built with JDK 25")
+    if FORY_CORE_JDK25_ENTRY not in names:
+        raise RuntimeError(f"{jar_path} is missing {FORY_CORE_JDK25_ENTRY}")
+    javap = subprocess.run(
+        [
+            _java_tool("javap"),
+            "--multi-release",
+            "25",
+            "-classpath",
+            jar_path,
+            "-p",
+            FORY_CORE_ACCESSOR,
+        ],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        universal_newlines=True,
+        check=True,
+    )
+    if "java.lang.invoke.VarHandle" not in javap.stdout:
+        raise RuntimeError(f"{FORY_CORE_ACCESSOR} is not the JDK25 VarHandle 
class")
+    if "sun.misc.Unsafe" in javap.stdout:
+        raise RuntimeError(f"{FORY_CORE_ACCESSOR} still exposes 
sun.misc.Unsafe")
+    logger.info("Verified fory-core Multi-Release JDK25 jar: %s", jar_path)
+
+
+def _fory_core_jar_path():
+    version = _read_java_version()
+    return os.path.join(
+        PROJECT_ROOT_DIR,
+        "java",
+        "fory-core",
+        "target",
+        f"fory-core-{version}.jar",
+    )
+
+
+def _read_java_version():
+    pom = os.path.join(PROJECT_ROOT_DIR, "java", "pom.xml")
+    root = ET.parse(pom).getroot()
+    namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
+    artifact = root.findtext("m:artifactId", namespaces=namespace)
+    packaging = root.findtext("m:packaging", namespaces=namespace)
+    version = root.findtext("m:version", namespaces=namespace)
+    if artifact != "fory-parent" or packaging != "pom" or not version:
+        raise ValueError("Cannot find java/fory parent version")
+    return version
+
+
 def bump_version(**kwargs):
     new_version = kwargs["version"]
     langs = kwargs["l"]
@@ -801,6 +1071,37 @@ def _parse_args():
     verify_parser.add_argument("-v", type=str, help="new version")
     verify_parser.set_defaults(func=verify)
 
+    publish_jvm_parser = subparsers.add_parser(
+        "publish_jvm",
+        description="Publish Java, Kotlin, and Scala artifacts",
+    )
+    publish_jvm_parser.add_argument(
+        "-l",
+        dest="languages",
+        type=str,
+        default="all",
+        help="comma separated JVM languages: java,kotlin,scala",
+    )
+    publish_jvm_parser.set_defaults(func=publish_jvm)
+
+    publish_java_parser = subparsers.add_parser(
+        "publish_java",
+        description="Publish Java artifacts",
+    )
+    publish_java_parser.set_defaults(func=publish_java)
+
+    publish_kotlin_parser = subparsers.add_parser(
+        "publish_kotlin",
+        description="Publish Kotlin artifacts",
+    )
+    publish_kotlin_parser.set_defaults(func=publish_kotlin)
+
+    publish_scala_parser = subparsers.add_parser(
+        "publish_scala",
+        description="Publish Scala artifacts",
+    )
+    publish_scala_parser.set_defaults(func=publish_scala)
+
     args = parser.parse_args()
     arg_dict = dict(vars(args))
     del arg_dict["func"]
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java 
b/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
index a665284fb..d1d3ed029 100644
--- a/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
+++ b/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
@@ -29,7 +29,7 @@ import java.time.format.DateTimeFormatter;
 
 public class ForyLogger implements Logger {
   private static final DateTimeFormatter dateTimeFormatter =
-      DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
+      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
   private final String name;
 
   public ForyLogger(Class<?> targetClass) {
diff --git a/java/pom.xml b/java/pom.xml
index 3fa4e4ce0..0feb8c836 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -84,6 +84,36 @@
   </properties>
 
   <profiles>
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-enforcer-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>enforce-jdk25-release</id>
+                <phase>validate</phase>
+                <goals>
+                  <goal>enforce</goal>
+                </goals>
+                <configuration>
+                  <rules>
+                    <requireJavaVersion>
+                      <version>[25,26)</version>
+                      <message>
+                        Apache Fory JVM releases must run with JDK 25 so JDK25 
multi-release classes are packaged.
+                      </message>
+                    </requireJavaVersion>
+                  </rules>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
     <profile>
       <id>jdk11-and-higher</id>
       <activation>
diff --git a/kotlin/pom.xml b/kotlin/pom.xml
index 31764a812..0807429f3 100644
--- a/kotlin/pom.xml
+++ b/kotlin/pom.xml
@@ -51,6 +51,39 @@
         <maven-spotless-plugin.version>2.43.0</maven-spotless-plugin.version>
     </properties>
 
+    <profiles>
+        <profile>
+            <id>apache-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>enforce-jdk25-release</id>
+                                <phase>validate</phase>
+                                <goals>
+                                    <goal>enforce</goal>
+                                </goals>
+                                <configuration>
+                                    <rules>
+                                        <requireJavaVersion>
+                                            <version>[25,26)</version>
+                                            <message>
+                                                Apache Fory JVM releases must 
run with JDK 25 so JDK25 multi-release classes are packaged.
+                                            </message>
+                                        </requireJavaVersion>
+                                    </rules>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
     <build>
         <pluginManagement>
             <plugins>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to