Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ version = '5.44.0'

// CycloneDX SBOM generation configuration
cyclonedxBom {
// Production runtime dependencies are the contents of the standalone JAR;
// test and annotation configurations do not belong in the shipped SBOM.
includeConfigs = ["runtimeClasspath"]
projectType = "application"
schemaVersion = "1.5"
includeLicenseText = false
Expand Down Expand Up @@ -229,7 +232,7 @@ dependencies {
implementation libs.bcpkix // Bouncy Castle PEM/PKCS parsing
implementation libs.snappy.java // Official Sereal Java codec compression
implementation libs.zstd.jni // Official Sereal Java codec compression
implementation 'org.jruby.joni:joni:2.2.7' // Stack-safe recursive regex backend
implementation 'org.jruby.jcodings:jcodings:1.0.64' // Encoding support for vendored Joni
implementation 'io.netty:netty-codec-http:4.1.115.Final' // Netty HTTP codec for PSGI server

// Testing dependencies
Expand Down Expand Up @@ -343,13 +346,41 @@ shadowJar {
}
exclude 'module-info.class'
exclude 'META-INF/MANIFEST.MF'

// Keep the embedded regex engine private when PerlOnJava shares a
// classpath with JRuby or another Joni/JCodings version.
relocate 'org.joni', 'org.perlonjava.internal.joni'
relocate 'org.jcodings', 'org.perlonjava.internal.jcodings'

from('third_party/joni/LICENSE') {
into 'META-INF/licenses'
rename { 'joni-LICENSE.txt' }
}
from('third_party/licenses/jcodings-LICENSE.txt') {
into 'META-INF/licenses'
}
from('third_party/joni/PERLONJAVA-NOTICE.md') {
into 'META-INF/licenses'
rename { 'joni-PERLONJAVA-NOTICE.md' }
}

// Include combined SBOM in JAR's META-INF/sbom/ directory
from("$buildDir/reports/sbom.json") {
into 'META-INF/sbom'
}
}

tasks.register('verifyJoniPackaging', Exec) {
description = 'Verifies Joni namespace isolation, notices, and SBOM metadata'
group = 'verification'
dependsOn shadowJar
inputs.file("dev/tools/verify-joni-packaging.pl")
inputs.file("target/perlonjava-${project.version}.jar")
inputs.file("build/reports/sbom.json")
commandLine 'perl', 'dev/tools/verify-joni-packaging.pl',
"target/perlonjava-${project.version}.jar", 'build/reports/sbom.json'
}

// Some Perl unit tests spawn a child interpreter via $^X. Under Gradle those
// children go through the repository launcher, so the target jar must be built
// before tests run; otherwise a stale target/perlonjava-*.jar can be executed.
Expand Down Expand Up @@ -392,6 +423,7 @@ tasks.register('mergeSbom', Exec) {
// Declare inputs so Gradle knows dependencies
inputs.file("build/reports/bom.json")
inputs.file("build/reports/perl-bom.json")
inputs.file("dev/tools/merge-sbom.pl")

workingDir = projectDir
commandLine 'perl', 'dev/tools/merge-sbom.pl',
Expand Down Expand Up @@ -420,6 +452,11 @@ tasks.named('build') {
// Source sets configuration for including Perl resources
sourceSets {
main {
java {
// Start below Joni's optional Java 9 module descriptor so the
// otherwise classpath-based PerlOnJava build remains unnamed.
srcDir 'third_party/joni/src/org'
}
resources {
srcDir 'src/main/perl'
srcDir 'src/main/resources'
Expand All @@ -437,13 +474,35 @@ sourceSets {
include 'META-INF/services/**'
}
}
joniTest {
java.srcDir 'third_party/joni/test'
compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath
runtimeClasspath += output + compileClasspath
}
test {
resources {
srcDir 'src/test/resources'
}
}
}

configurations {
joniTestImplementation.extendsFrom testImplementation
joniTestRuntimeOnly.extendsFrom testRuntimeOnly
}

dependencies {
joniTestImplementation 'junit:junit:4.13.2'
}

tasks.register('testJoni', Test) {
description = 'Runs the imported upstream Joni test suite'
group = 'verification'
testClassesDirs = sourceSets.joniTest.output.classesDirs
classpath = sourceSets.joniTest.runtimeClasspath
useJUnit()
}

// Resource processing configuration
tasks.named('processResources', Copy) {
from(sourceSets.main.resources.srcDirs) {
Expand Down Expand Up @@ -498,6 +557,8 @@ tasks.register('testUnitParallel') {
description = 'Runs unit tests in parallel across multiple JVMs. Usage: gradle testUnitParallel --parallel'
def shardTasks = (0..<parallelShards).collect { "testUnitShard${it}" }
dependsOn shardTasks
dependsOn testJoni
dependsOn verifyJoniPackaging
}

// Version catalog update configuration
Expand Down
44 changes: 44 additions & 0 deletions dev/tools/merge-sbom.pl
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@
my @all_components;
my @root_deps;

my $joni_ref = 'pkg:maven/org.jruby.joni/joni@2.2.7?type=jar';
my $jcodings_ref = 'pkg:maven/org.jruby.jcodings/jcodings@1.0.64?type=jar';

# Add Java components
if ($java_bom->{components}) {
for my $comp (@{$java_bom->{components}}) {
Expand All @@ -97,6 +100,43 @@
}
}

# Joni is compiled from the pinned source snapshot in third_party/joni rather
# than resolved as a binary dependency, so CycloneDX cannot discover it from
# runtimeClasspath. Record the vendored component explicitly.
push @all_components, {
type => 'library',
'bom-ref' => $joni_ref,
group => 'org.jruby.joni',
name => 'joni',
version => '2.2.7',
description => 'Vendored Java port of Oniguruma with PerlOnJava callout extensions',
licenses => [
{
license => {
id => 'MIT'
}
}
],
purl => $joni_ref,
externalReferences => [
{
type => 'website',
url => 'https://github.com/jruby/joni'
},
{
type => 'vcs',
url => 'https://github.com/jruby/joni.git'
}
],
properties => [
{
name => 'perlonjava:vendored',
value => 'true'
}
]
};
push @root_deps, $joni_ref;

# Add Perl components
if ($perl_bom->{components}) {
for my $comp (@{$perl_bom->{components}}) {
Expand All @@ -112,6 +152,10 @@
{
ref => 'perlonjava',
dependsOn => \@root_deps
},
{
ref => $joni_ref,
dependsOn => [$jcodings_ref]
}
];

Expand Down
49 changes: 49 additions & 0 deletions dev/tools/verify-joni-packaging.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env perl

use strict;
use warnings;
use JSON::PP;

die "Usage: $0 <standalone.jar> <sbom.json>\n" unless @ARGV == 2;
my ($jar_file, $sbom_file) = @ARGV;

my %entries;
open my $jar_fh, '-|', 'jar', 'tf', $jar_file
or die "Cannot list $jar_file: $!\n";
while (my $entry = <$jar_fh>) {
chomp $entry;
$entries{$entry} = 1;
}
close $jar_fh or die "Cannot list $jar_file: jar exited with status $?\n";

die "Standalone JAR contains unrelocated Joni classes\n"
if grep { m{^org/joni/} } keys %entries;
die "Standalone JAR contains unrelocated JCodings classes\n"
if grep { m{^org/jcodings/} } keys %entries;
die "Standalone JAR does not contain relocated Joni classes\n"
unless grep { m{^org/perlonjava/internal/joni/} } keys %entries;
die "Standalone JAR does not contain relocated JCodings classes\n"
unless grep { m{^org/perlonjava/internal/jcodings/} } keys %entries;

for my $notice (qw(
joni-LICENSE.txt
jcodings-LICENSE.txt
joni-PERLONJAVA-NOTICE.md
)) {
die "Standalone JAR is missing $notice\n"
unless $entries{"META-INF/licenses/$notice"};
}

open my $sbom_fh, '<', $sbom_file or die "Cannot open $sbom_file: $!\n";
local $/;
my $sbom = JSON::PP->new->utf8->decode(<$sbom_fh>);
close $sbom_fh;

my ($joni) = grep {
($_->{group} // '') eq 'org.jruby.joni'
&& ($_->{name} // '') eq 'joni'
&& ($_->{version} // '') eq '2.2.7'
} @{$sbom->{components} // []};
die "Combined SBOM is missing vendored Joni 2.2.7\n" unless $joni;

print "Joni packaging verification passed\n";
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Looking to contribute? See:

- **[CONTRIBUTING.md](../CONTRIBUTING.md)** - Contribution guidelines
- **[dev/](../dev/)** - Developer documentation and internal architecture
- **[Joni Callout Fork](design/joni-callout-fork.md)** - Vendored regex engine design and implementation contract

## Finding What You Need

Expand Down
Loading
Loading