Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b98a6cb
docs: add DBIx::Class fix plan
fglock Mar 31, 2026
69a7058
feat: implement strict::bits, all_bits, all_explicit_bits
fglock Mar 31, 2026
c4061e9
fix: UNIVERSAL::can must not consider AUTOLOAD-dispatched methods
fglock Mar 31, 2026
68ac9e1
fix: goto &sub now propagates wantarray context + eval{} shares @_ wi…
fglock Mar 31, 2026
def5bff
fix: parse +{} as hash constructor inside %{+{@a}} deref context
fglock Mar 31, 2026
19c0544
docs: update DBIx::Class fix plan - Phase 1 complete, Phase 2 started
fglock Mar 31, 2026
927a56b
fix: CORE::GLOBAL:: override fails when caller is RHS of operator
fglock Mar 31, 2026
dc54a81
feat: implement strict::bits, all_bits, all_explicit_bits
fglock Mar 31, 2026
5498fb6
fix: UNIVERSAL::can must not consider AUTOLOAD-dispatched methods
fglock Mar 31, 2026
2e33154
fix: goto &sub now propagates wantarray context + eval{} shares @_ wi…
fglock Mar 31, 2026
20a9dfe
fix: parse +{} as hash constructor inside %{+{@a}} deref context
fglock Mar 31, 2026
fc0b35f
fix: CORE::GLOBAL:: override fails when caller is RHS of operator
fglock Mar 31, 2026
08c1137
fix: stash aliasing glob vivification for Package::Stash::PP
fglock Apr 1, 2026
4ffd623
fix: allow mixed-context ternary lvalues like (cond ? @arr : $scalar)…
fglock Apr 1, 2026
1c13178
fix: version comparison, isPackageLoaded, and cp overwrite bugs
fglock Apr 1, 2026
d9492da
fix: autoquote -KEYWORD in hash subscripts ($h{-join}, $h{-sort}, etc.)
fglock Apr 1, 2026
9f0d349
fix: parse $-prototype functions as named unary operators
fglock Apr 1, 2026
5f03d4f
fix: isweak() returns true for references (JVM GC handles cycles)
fglock Apr 1, 2026
276528a
fix: support @${$v} interpolation in double-quoted strings
fglock Apr 1, 2026
e30aa06
fix: add B::SV::REFCNT method returning 0 (JVM tracing GC)
fglock Apr 1, 2026
cdb9239
fix: DBI FETCH/STORE methods, autovivification in list assignment
fglock Apr 1, 2026
8d35a94
fix: add DBI execute_for_fetch and bind_param methods
fglock Apr 1, 2026
7c4f714
fix: &func (no parens) now shares caller @_ by alias
fglock Apr 1, 2026
c142c53
fix: DBI execute() returns row count per DBI spec instead of hash ref
fglock Apr 1, 2026
e3fcf62
docs: update DBIx::Class plan with Phase 5 progress
fglock Apr 1, 2026
0a16e6d
docs: add t/00describe_environment.t investigation to plan
fglock Apr 1, 2026
a76aebc
fix: DBI driver detection, get_info, SQL constants, bind_columns
fglock Apr 1, 2026
db3ba55
fix: prevent VerifyError from JVM local variable slot reuse across br…
fglock Apr 1, 2026
96eed12
fix: restore mixed-context ternary lvalue error, match Perl 5 S_assig…
fglock Apr 1, 2026
3a0d8d5
docs: update Phase 4.7 in DBIx::Class plan with S_assignment_type fix…
fglock Apr 1, 2026
ffd100c
fix: prototype parsing, signature defaults, and utf8::upgrade regression
fglock Apr 1, 2026
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ PerlOnJava does **not** implement the following Perl features:

### Testing

**NEVER modify or delete existing tests.** Tests are the source of truth. If a test fails, fix the code, not the test. When in doubt, verify expected behavior with system Perl (`perl`, not `jperl`).

**ALWAYS use `make` commands. NEVER use raw mvn/gradlew commands.**

| Command | What it does |
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ dependencies {
implementation libs.snakeyaml.engine // YAML processing
implementation libs.tomlj // TOML processing
implementation libs.commons.csv // CSV processing
implementation libs.sqlite.jdbc // SQLite JDBC driver
// JNR-POSIX removed - using Java FFM API for native access (Java 22+)

// Testing dependencies
Expand Down
2 changes: 2 additions & 0 deletions dev/modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This directory contains design documents and guides related to porting CPAN modu
| [xsloader.md](xsloader.md) | XSLoader architecture |
| [makemaker_perlonjava.md](makemaker_perlonjava.md) | ExtUtils::MakeMaker implementation |
| [cpan_client.md](cpan_client.md) | jcpan - CPAN client for PerlOnJava |
| [dbix_class.md](dbix_class.md) | DBIx::Class support (in progress) |

## Module Status Overview

Expand Down Expand Up @@ -117,5 +118,6 @@ PERL_PARAMS_UTIL_PP=1 ./jcpan -t Class::Load
- [moose_support.md](moose_support.md) - Moose support (in progress)
- [moo_support.md](moo_support.md) - Moo support (working)
- [JCPAN_DATETIME_FIXES.md](JCPAN_DATETIME_FIXES.md) - DateTime via jcpan
- [dbix_class.md](dbix_class.md) - DBIx::Class support
- [log4perl-compatibility.md](log4perl-compatibility.md) - Log::Log4perl
- [term_readkey.md](term_readkey.md) - Term::ReadKey
370 changes: 370 additions & 0 deletions dev/modules/dbix_class.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ fastjson2 = "2.0.61"
icu4j = "78.3"
junit-jupiter = "6.1.0-M1"
snakeyaml-engine = "3.0.1"
sqlite-jdbc = "3.49.1.0"
tomlj = "1.1.1"

[libraries]
Expand All @@ -17,6 +18,7 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" }
snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version.ref = "snakeyaml-engine" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" }
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }

[plugins]
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.49.1.0</version>
</dependency>
<!-- JNR-POSIX removed - using Java FFM API for native access (Java 22+) -->
</dependencies>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -834,10 +834,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
// SUBROUTINE CALLS
// =================================================================

case Opcodes.CALL_SUB -> {
case Opcodes.CALL_SUB, Opcodes.CALL_SUB_SHARE_ARGS -> {
// Call subroutine: rd = coderef->(args)
// CALL_SUB_SHARE_ARGS: &func (no parens) shares caller's @_ by alias
// May return RuntimeControlFlowList!
// pcHolder[0] contains the PC of this opcode (set before opcode read)
boolean shareArgs = (opcode == Opcodes.CALL_SUB_SHARE_ARGS);
// pcHolder[0] contains the PC of this opcode (set before opcode read)
int callSitePc = pcHolder[0];
int rd = bytecode[pc++];
int coderefReg = bytecode[pc++];
Expand Down Expand Up @@ -893,7 +896,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
}
} else {
// Slow path for JVM-compiled code, symbolic references, etc.
result = RuntimeCode.apply(codeRef, "", callArgs, context);
// For &func (shareArgs), use the apply overload that shares @_
if (shareArgs) {
result = RuntimeCode.apply(codeRef, callArgs, context);
} else {
result = RuntimeCode.apply(codeRef, "", callArgs, context);
}
}

// Handle TAILCALL with trampoline loop (same as JVM backend)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,13 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.compileNode(node.right, -1, RuntimeContextType.LIST);
int rs2 = bytecodeCompiler.lastResultReg;

// Emit CALL_SUB opcode
int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, node.getIndex());
// Check if this is a &func (no parens) call that should share caller's @_
boolean shareCallerArgs = node.getBooleanAnnotation("shareCallerArgs");

// Emit CALL_SUB or CALL_SUB_SHARE_ARGS opcode
int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(
bytecodeCompiler, node.operator, rs1, rs2, node.getIndex(),
shareCallerArgs);
bytecodeCompiler.lastResultReg = rd;
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public class CompileBinaryOperatorHelper {
* @return Result register containing the operation result
*/
public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, String operator, int rs1, int rs2, int tokenIndex) {
return compileBinaryOperatorSwitch(bytecodeCompiler, operator, rs1, rs2, tokenIndex, false);
}

public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, String operator, int rs1, int rs2, int tokenIndex, boolean shareCallerArgs) {
// Allocate result register
int rd = bytecodeCompiler.allocateOutputRegister();

Expand Down Expand Up @@ -215,7 +219,8 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler,
// BytecodeInterpreter convert it to RuntimeArray

// Emit CALL_SUB: rd = coderef.apply(args, context)
bytecodeCompiler.emit(Opcodes.CALL_SUB);
// Use CALL_SUB_SHARE_ARGS for &func (no parens) to share caller's @_
bytecodeCompiler.emit(shareCallerArgs ? Opcodes.CALL_SUB_SHARE_ARGS : Opcodes.CALL_SUB);
bytecodeCompiler.emitReg(rd); // Result register
bytecodeCompiler.emitReg(rs1); // Code reference register
bytecodeCompiler.emitReg(rs2); // Arguments register (RuntimeList to be converted to RuntimeArray)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,11 +875,13 @@ public static String disassemble(InterpretedCode interpretedCode) {
break;
}
case Opcodes.CALL_SUB:
case Opcodes.CALL_SUB_SHARE_ARGS:
rd = interpretedCode.bytecode[pc++];
int coderefReg = interpretedCode.bytecode[pc++];
int argsReg = interpretedCode.bytecode[pc++];
int ctx = interpretedCode.bytecode[pc++];
sb.append("CALL_SUB r").append(rd).append(" = r").append(coderefReg)
sb.append(opcode == Opcodes.CALL_SUB_SHARE_ARGS ? "CALL_SUB_SHARE_ARGS r" : "CALL_SUB r")
.append(rd).append(" = r").append(coderefReg)
.append("->(r").append(argsReg).append(", ctx=").append(ctx).append(")\n");
break;
case Opcodes.CALL_METHOD:
Expand Down
37 changes: 22 additions & 15 deletions src/main/java/org/perlonjava/backend/bytecode/Opcodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -2040,28 +2040,35 @@ public class Opcodes {
// Effect: rd = CompareOperators.smartmatch(rs1, rs2)
public static final short SMARTMATCH = 400;

/**
* Call subroutine sharing caller's @_: rd = RuntimeCode.apply(coderef_reg, args_reg, context)
* Used for &func (no parens) which shares caller's @_ by alias.
* Same format as CALL_SUB but uses the sharing apply() overload in slow path.
*/
public static final short CALL_SUB_SHARE_ARGS = 401;

// Missing system operators needed for interpreter fallback of large files (e.g. taint.t)
public static final short SYMLINK = 401;
public static final short CHROOT = 402;
public static final short MKDIR = 403;
public static final short MSGCTL = 404;
public static final short SHMCTL = 405;
public static final short SEMCTL = 406;
public static final short EXEC = 407;
public static final short FCNTL = 408;
public static final short IOCTL = 409;
public static final short GETPWENT = 410;
public static final short SETPWENT = 411;
public static final short ENDPWENT = 412;
public static final short SYMLINK = 402;
public static final short CHROOT = 403;
public static final short MKDIR = 404;
public static final short MSGCTL = 405;
public static final short SHMCTL = 406;
public static final short SEMCTL = 407;
public static final short EXEC = 408;
public static final short FCNTL = 409;
public static final short IOCTL = 410;
public static final short GETPWENT = 411;
public static final short SETPWENT = 412;
public static final short ENDPWENT = 413;

/**
* Dynamic loop control: last/next/redo with runtime-evaluated label expression.
* Format: CREATE_LAST_DYNAMIC rd labelReg
* Creates RuntimeControlFlowList with label from registers[labelReg].toString().
*/
public static final short CREATE_LAST_DYNAMIC = 413;
public static final short CREATE_NEXT_DYNAMIC = 414;
public static final short CREATE_REDO_DYNAMIC = 415;
public static final short CREATE_LAST_DYNAMIC = 443;
public static final short CREATE_NEXT_DYNAMIC = 444;
public static final short CREATE_REDO_DYNAMIC = 445;

// ExtendedNativeUtils operators (user/group info, network lookups, enumeration)
public static final short GETLOGIN = 416;
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/Dereference.java
Original file line number Diff line number Diff line change
Expand Up @@ -959,14 +959,19 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
}
}

// Save the call context into a local slot for the TAILCALL trampoline.
int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ISTORE, callContextSlot);

// Allocate a unique callsite ID for inline method caching
int callsiteId = nextMethodCallsiteId++;
mv.visitLdcInsn(callsiteId);
mv.visitVarInsn(Opcodes.ALOAD, objectSlot);
mv.visitVarInsn(Opcodes.ALOAD, methodSlot);
mv.visitVarInsn(Opcodes.ALOAD, subSlot);
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);
emitterVisitor.pushCallContext(); // push call context to stack (handles RUNTIME)
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // push saved call context
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
Expand Down Expand Up @@ -1048,7 +1053,7 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallCodeRefSlot);
mv.visitLdcInsn("tailcall");
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallArgsSlot);
mv.visitVarInsn(Opcodes.ILOAD, 2); // context parameter (passed to current sub)
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // context of the original call site
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
Expand Down
65 changes: 64 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,69 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
}
mv.visitVarInsn(Opcodes.ASTORE, codeRefSlot);

// Special handling for eval blocks: share @_ with enclosing sub directly.
// In Perl 5, eval { } shares @_ with its enclosing sub, so shift/pop inside
// eval { } modifies the caller's @_. We achieve this by passing the caller's
// RuntimeArray directly instead of expanding @_ into a new array.
// Note: use apply() not applyEval() because the eval block's own generated
// method already has try/catch handling (useTryCatch=true). Using applyEval
// would add a second layer that clears $@ after the block returns.
if (node.left instanceof SubroutineNode subNode && subNode.useTryCatch) {
mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot);
mv.visitVarInsn(Opcodes.ALOAD, 1); // caller's @_ (slot 1) - shared, not copied
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);

if (pooledCodeRef) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}

if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) {
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false);
} else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
mv.visitInsn(Opcodes.POP);
}
return;
}

// Special handling for &func (no parens): share @_ with caller directly.
// In Perl 5, &func without parens shares the caller's @_ by alias,
// so shift/pop inside the callee modifies the caller's @_.
// We achieve this by passing the caller's RuntimeArray (slot 1) directly
// instead of creating a new array from @_ elements.
if (node.getBooleanAnnotation("shareCallerArgs")) {
mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot);
mv.visitVarInsn(Opcodes.ALOAD, 1); // caller's @_ (slot 1) - shared, not copied
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);

if (pooledCodeRef) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}

// Registry-based non-local control flow check (for next/last/redo LABEL from closures)
emitControlFlowCheck(emitterVisitor.ctx);

if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) {
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false);
} else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
mv.visitInsn(Opcodes.POP);
}
return;
}

int nameSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
boolean pooledName = nameSlot >= 0;
if (!pooledName) {
Expand Down Expand Up @@ -594,7 +657,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallCodeRefSlot);
mv.visitLdcInsn("tailcall");
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallArgsSlot);
mv.visitVarInsn(Opcodes.ILOAD, 2); // context parameter (passed to current sub)
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // context of the original call site
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,15 @@ private static void fetchGlobalVariable(EmitterContext ctx, boolean createIfNotE
* @param node the OperatorNode representing the variable operation
*/
static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
// In void context, don't emit any code
if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
String sigil = node.operator;

// In void context, don't emit any code — EXCEPT for glob (*) access,
// which has vivification side effects. In Perl, `*{"PKG::name"}` in void
// context still vivifies the glob entry in the stash. Package::Stash::PP
// relies on this for its `local *__ANON__:: = $namespace; *{"__ANON__::$name"};` pattern.
if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID && !sigil.equals("*")) {
return;
}

String sigil = node.operator;
MethodVisitor mv = emitterVisitor.ctx.mv;

// Case 1: Simple variable with identifier (most common case)
Expand All @@ -363,6 +366,10 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n
"createDetachedCopy",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeGlob;",
false);
// In void context, pop the result — the access was needed for vivification side effects
if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
mv.visitInsn(Opcodes.POP);
}
return;
}

Expand Down Expand Up @@ -555,6 +562,11 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n
emitterVisitor.pushCurrentPackage();
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar", "globDerefNonStrict", "(Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeGlob;", false);
}
// In void context, pop the result off the JVM stack since no one consumes it.
// The glob access was still needed for its vivification side effect.
if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
mv.visitInsn(Opcodes.POP);
}
return;
case "&":
// `&$a` or `&{sub ...}`
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "80afde768";
public static final String gitCommitId = "509cc4f94";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Loading
Loading