plugins { id 'com.github.node-gradle.node' version '7.1.0' id "java" id "war" id "org.teavm" version "${teaVmVersion}" } import java.security.MessageDigest import java.nio.file.Files import java.nio.file.Paths apply from: "${rootDir}/gradle/libs/class-finder.gradle" configurations { withResources { canBeResolved = true } } node { download = true distBaseUrl = null version = "22.18.0" } configurations.configureEach { exclude group: 'org.ngengine', module: 'jme3-jbullet' } dependencies { implementation "org.teavm:teavm-jso:${teaVmVersion}" implementation "org.teavm:teavm-jso-apis:${teaVmVersion}" implementation "org.teavm:teavm-metaprogramming-api:${teaVmVersion}" implementation "org.teavm:teavm-classlib:${teaVmVersion}" implementation "org.teavm:teavm-tooling:${teaVmVersion}" implementation "org.teavm:teavm-core:${teaVmVersion}" implementation "org.ngengine:nge-platform-teavm:${ngePlatformVersion}" implementation "org.ngengine:nge-app:${ngeVersion}" implementation "org.ngengine:jme3-plugins-json:${ngeVersion}" implementation "org.ngengine:nge-web:${ngeVersion}" implementation "org.ngengine:nge-web-bullet:${ngeVersion}" implementation project(':app') withResources project(':app') withResources "org.ngengine:nge-platform-teavm:${ngePlatformVersion}" withResources "org.ngengine:nge-web:${ngeVersion}" withResources "org.ngengine:nge-web-bullet:${ngeVersion}" } java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } sourceSets { test { resources { srcDirs = ['src/test/resources', 'src/main/resources'] } } } def binaryExclusions = [ '**/*.class', '**/*.dll', '**/*.so', '**/*.dylib', '**/package.html', "**/*.kotlin_builtins", "**/build.data", "**/*.xml", "**/test*.png", // '**/*.java' ] def generateHashes(File dir) { def hashes = [] Files.walk(Paths.get(dir.toURI())).filter { Files.isRegularFile(it) }.forEach { file -> def relativePath = dir.toPath().relativize(file).toString() if(relativePath.endsWith(".class")) return; if(relativePath.endsWith(".java")) return; def hash = generateSHA256(file.toFile()) def size = file.toFile().length() hashes << "$hash $size $relativePath" } return hashes.join('\n') } def generateSHA256(file) { def messageDigest = MessageDigest.getInstance("SHA-256") file.eachByte(4096) { bytes, len -> messageDigest.update(bytes, 0, len) } return messageDigest.digest().collect { String.format("%02x", it) }.join() } task copyAllResources { dependsOn configurations.runtimeClasspath.getTaskDependencyFromProjectDependency(true, 'jar') dependsOn 'jar' ['compileTeavmJava','processTeavmResources','compileJava','processResources'] .each { tn -> if (tasks.findByName(tn) != null) dependsOn tn } doLast { def dest = file("${rootDir}/dist/web") dest.mkdirs() def safeCopy = { spec -> copy { into dest from spec exclude(*binaryExclusions) exclude '**/module-info.class' includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } } safeCopy('src/main/resources') def runtimeFiles = configurations.runtimeClasspath.resolvedConfiguration.lenientConfiguration.files runtimeFiles.each { f -> if (!f.exists()) return if (f.isDirectory()) { safeCopy(f) } else if (f.name.endsWith('.jar')) { safeCopy(project.zipTree(f)) } } println "Resource aggregation complete. Total files: " + fileTree(dest).matching { exclude(*binaryExclusions) }.files.size() def manifestFile = file("${rootDir}/dist/web/manifest.json") if (manifestFile.exists()) { def manifest = new groovy.json.JsonSlurper().parseText(manifestFile.text) manifest.name = Title manifest.short_name = ShortName manifestFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(manifest)) println "Updated manifest.json: name=$Title, short_name=$ShortName" } def outputDir = file("${rootDir}/dist/web/") def outputFile = file("${rootDir}/dist/web/resources.index.txt") outputFile.text = generateHashes(outputDir) } } def generateClassListFile(File outputFile, Collection classes, Closure lineFormatter) { outputFile.parentFile.mkdirs() outputFile.text = classes.collect(lineFormatter).join('\n') println "Generated class list at ${outputFile.absolutePath}" } def generateCodeFromTemplate(File outputFile, String template, Map vars) { outputFile.parentFile.mkdirs() String result = template vars.each { key, value -> result = result.replace("{{$key}}", value.toString()) } outputFile.text = result println "Generated code at ${outputFile.absolutePath}" } task scanClasspathForTeaVM { dependsOn configurations.compileClasspath.getTaskDependencyFromProjectDependency(true, 'classes') doLast { def scanResults = project.scanClasspathForClasses(project) def preserveListFile = file("${buildDir}/generated/teaReflectList.txt") generateClassListFile(preserveListFile, scanResults.classes) { className -> "**/${className.replace('.', '/')}.java" } def reflectionSupplierFile = file("${buildDir}/generated/teaReflect.java") def template = """private static void initGenerated() { {{classInitializers}} }""" generateCodeFromTemplate(reflectionSupplierFile, template, [ classInitializers: scanResults.reflectionClasses.collect { " TeaReflectionSupplier.addReflectionClass(\"${it}\");" }.join('\n') ]) } } task updateTeaReflectionSupplier { dependsOn scanClasspathForTeaVM doLast { def reflectionSupplierFile = file("${buildDir}/generated/teaReflect.java") def teaReflectionSupplierFile = file("${projectDir}/src/main/java/org/ngengine/platform/TeaReflectionSupplier.java") if (reflectionSupplierFile.exists() && teaReflectionSupplierFile.exists()) { def content = teaReflectionSupplierFile.text def initGeneratedContent = reflectionSupplierFile.text content = content.replaceAll(/(?s)private static void initGenerated\(\)\s*\{.*?\}/, initGeneratedContent) teaReflectionSupplierFile.text = content println "Updated TeaReflectionSupplier.java with generated initGenerated() method" } } } tasks.named('compileJava').configure { dependsOn 'updateTeaReflectionSupplier' } teavm.js { addedToWebApp = false mainClass = "org.ngengine.platform.WebLauncher" obfuscated = false debugInformation = true strict= false outOfProcess=false // optimization=org.teavm.gradle.api.OptimizationLevel.NONE sourceMap=true moduleType = org.teavm.gradle.api.JSModuleType.ES2015 def classesToPreserve=[] def preserveListFile = file("${buildDir}/generated/teaReflectList.txt") if (preserveListFile.exists()) { preserveListFile.readLines().each { line -> if (line.trim()) { classesToPreserve.add(line.trim()) } } } def fqcnToPreserve = new LinkedHashSet() def toFqcn = { File baseDir, File f -> def rel = f.path.substring(baseDir.path.length() + 1) rel = rel.replaceAll(/\.java$/, '').replaceAll(/\.class$/, '') rel.replace('/', '.') } rootProject.allprojects.each { prj -> def ss = prj.extensions.findByName('sourceSets') def mainSS = ss?.findByName('main') if (mainSS == null) return mainSS.java.srcDirs.each { srcDir -> if (!srcDir.exists()) return fileTree(srcDir).matching { include classesToPreserve }.forEach { f -> def fqcn = toFqcn(srcDir, f) fqcnToPreserve.add(fqcn) } } } def rtArtifacts = configurations.runtimeClasspath.resolvedConfiguration.lenientConfiguration.files rtArtifacts.each { artifact -> if (!artifact.exists()) return if (artifact.isDirectory()) { fileTree(artifact).matching { include classesToPreserve.collect { it.replace('.java', '.class') } }.forEach { f -> def fqcn = toFqcn(artifact, f) fqcnToPreserve.add(fqcn) } } else if (artifact.name.endsWith('.jar')) { def classFilePatterns = classesToPreserve.collect { it.replace('.java', '.class') } .collect { it.replace('**/', '') } try { def jarFile = new java.util.jar.JarFile(artifact) def entries = jarFile.entries() while (entries.hasMoreElements()) { def entry = entries.nextElement() if (entry.isDirectory()) continue def entryName = entry.getName() if (classFilePatterns.any { pattern -> entryName.endsWith(pattern) || entryName.matches(pattern.replace("*", ".*")) }) { def fqcn = entryName.replaceAll(/\.class$/, '').replace('/', '.') fqcnToPreserve.add(fqcn) } } jarFile.close() } catch (Exception e) { println "WARNING: Could not process JAR file: ${artifact.name} - ${e.message}" } } } fqcnToPreserve.each { preservedClasses.add(it) } targetFileName = "webapp.js" } task buildWebApp { dependsOn 'compileJava', 'generateJavaScript', 'copyAllResources' doLast { def jsDir = file("$buildDir/generated/teavm/js") def distDir = file("${rootDir}/dist/web") distDir.mkdirs() copy { from jsDir into distDir } println "Copied web app to: $distDir" } } task buildBundledWebApp { dependsOn buildWebApp doLast { def jsDir = file("$rootDir/dist/web") def bundledJsDir = file("$rootDir/dist/web-bundle") def bundleIgnoreFile = file("$jsDir/bundle-ignore.txt") def zipFile = file("$bundledJsDir/ngeapp-bundle.zip") def configFile = file("$bundledJsDir/ngeapp.json") bundledJsDir.mkdirs() // Read ignore patterns from bundle-ignore.txt def ignorePatterns = [] if (bundleIgnoreFile.exists()) { bundleIgnoreFile.eachLine { line -> if (line.trim() && !line.startsWith('//')) ignorePatterns.add(line.trim()) } println "Found ${ignorePatterns.size()} patterns to exclude from bundle" } else { println "Warning: bundle-ignore.txt not found at ${bundleIgnoreFile}. No files will be excluded from the bundle." } // Create zip with everything except ignored files ant.zip(destfile: zipFile) { fileset(dir: jsDir) { ignorePatterns.each { pattern -> if (pattern.endsWith('/')) { exclude(name: "${pattern}**") } else { exclude(name: pattern) } } } } // Copy ignored files to bundledjs directory if (!ignorePatterns.isEmpty()) { println "Copying excluded files to bundled directory..." delete fileTree(bundledJsDir).matching { include '**/*' exclude 'ngeapp-bundle.zip', 'ngeapp-config.json' } fileTree(jsDir).visit { fileDetails -> if (fileDetails.isDirectory()) return def relativePath = fileDetails.relativePath.toString() def matchesPattern = false for (pattern in ignorePatterns) { if (pattern.endsWith('/')) { if (relativePath.startsWith(pattern) || relativePath.startsWith(pattern.substring(0, pattern.length() - 1))) { matchesPattern = true break } } else { if (relativePath == pattern) { matchesPattern = true break } } } if (matchesPattern) { println "Copying excluded file: $relativePath" copy { from fileDetails.file into new File(bundledJsDir, fileDetails.relativePath.parent.toString()) } } } println "Finished copying excluded files" } // Calculate hash of zip file def hash = generateSHA256(zipFile) // Set bundle in config file def config = [:] if (configFile.exists()) { try { config = new groovy.json.JsonSlurper().parseText(configFile.text) println "Updating existing config file: $configFile.path" } catch (Exception e) { println "Warning: Could not parse existing config file: $e.message" config = [:] } } config.bundle = "ngeapp-bundle.zip" config.bundleHash = hash configFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(config)) println "Created bundle: $zipFile.path" println "Bundle hash: $hash" println "Config written to: $configFile.path" } } task npmCi(type: NpmTask) { workingDir = file("${projectDir}") args = ['ci'] } task runWebApp(type: NpxTask) { dependsOn buildWebApp workingDir = file("$rootDir/dist/web") command = 'http-server' args = [ '.', '-p', '8000'] } task runBundledWebApp(type: NpxTask) { dependsOn buildBundledWebApp workingDir = file("$rootDir/dist/web-bundle") command = 'http-server' args = [ '.', '-p', '8000'] } task deployWebApp(type: NpxTask) { dependsOn buildBundledWebApp dependsOn npmCi command = 'nostr-deploy-cli' args = ['deploy','--skip-setup', '-d', "${rootDir}/dist/web-bundle"] workingDir = file("${projectDir}") }