package macros; #if macro import haxe.macro.Context; import haxe.macro.Expr; import sys.FileSystem; import sys.io.File; import haxe.Json; using StringTools; /** * Macro for processing @:Assets meta tags and building asset manifests */ @:keep class AssetMacro { // Track all assets processed private static var assetMap:Map> = new Map(); /** Force Assets to be typed, which invokes its @:build macro. */ public static function includeAssets():Void { Context.getType("Assets"); } /** * Entry point for the macro - called during compilation */ public static function build():Array { #if macro Sys.println("AssetMacro.build() called - macro is running"); Context.warning("AssetMacro is running", Context.currentPos()); #end // A compilation server can retain macro statics between builds. assetMap = new Map(); // Get current build context var fields = Context.getBuildFields(); var localClass = Context.getLocalClass().get(); #if macro Sys.println("Processing class: " + localClass.name); #end // Get the output directory from build.hxml var outputDir = getOutputDir(); var manifestDir = outputDir + "/manifest"; var assetsOutputDir = outputDir + "/assets"; // These directories are owned by this macro. Recreate them so removed or // renamed source assets cannot survive as stale build output. removeDirectory(manifestDir); removeDirectory(assetsOutputDir); createDirectory(manifestDir); createDirectory(assetsOutputDir); copyLimeNativeLibrary(outputDir); // Process asset fields for (field in fields) { processField(field, assetsOutputDir); } // Build the manifest files buildManifests(manifestDir); return fields; } /** * Process a field for @:Assets meta */ private static function processField(field:Field, assetsOutputDir:String) { // Look for our custom meta var assetsMeta = findAssetsMeta(field.meta); if (assetsMeta == null) return; var params = assetsMeta.params; if (params.length < 2) { Context.error("@:Assets meta requires at least 2 parameters: source path and library name", assetsMeta.pos); return; } // Extract parameters var sourcePath = extractString(params[0]); var libraryName = extractString(params[1]); var targetPath = params.length > 2 ? extractString(params[2]) : null; // Process directory or single file if (FileSystem.exists(sourcePath)) { if (FileSystem.isDirectory(sourcePath)) { processDirectory(sourcePath, libraryName, assetsOutputDir); } else { processSingleFile(sourcePath, libraryName, targetPath, assetsOutputDir); } } else { Context.warning('Asset source not found: ${sourcePath}', field.pos); } } /** * Process a directory of assets */ private static function processDirectory(sourcePath:String, libraryName:String, assetsOutputDir:String) { if (!sourcePath.endsWith("/")) sourcePath += "/"; var targetDir = assetsOutputDir + "/" + libraryName + "/"; createDirectory(targetDir); function scanDir(path:String, relPath:String = "") { if (!FileSystem.exists(path)) return; for (entry in FileSystem.readDirectory(path)) { var fullPath = path + entry; var relAssetPath = relPath + entry; if (FileSystem.isDirectory(fullPath)) { var newRelPath = relPath + entry + "/"; var newTargetDir = targetDir + newRelPath; createDirectory(newTargetDir); scanDir(fullPath + "/", newRelPath); } else { // Copy file and register in asset map var targetPath = targetDir + relAssetPath; try { File.copy(fullPath, targetPath); // Register asset if (!assetMap.exists(libraryName)) { assetMap.set(libraryName, []); } assetMap.get(libraryName).push({ id: relAssetPath, path: relAssetPath, size: FileSystem.stat(fullPath).size, type: getAssetType(fullPath) }); } catch (e) { Context.warning('Failed to copy asset: ${fullPath} -> ${targetPath}. Error: ${e}', Context.currentPos()); } } } } scanDir(sourcePath); } /** * Process a single file asset */ private static function processSingleFile(sourcePath:String, libraryName:String, targetPath:String, assetsOutputDir:String) { var targetDir = assetsOutputDir + "/" + libraryName + "/"; createDirectory(targetDir); var fileName = targetPath != null ? targetPath : sourcePath.split("/").pop(); var destPath = targetDir + fileName; try { File.copy(sourcePath, destPath); // Register asset if (!assetMap.exists(libraryName)) { assetMap.set(libraryName, []); } assetMap.get(libraryName).push({ id: fileName, path: fileName, size: FileSystem.stat(sourcePath).size, type: getAssetType(sourcePath) }); } catch (e) { Context.warning('Failed to copy asset: ${sourcePath} -> ${destPath}. Error: ${e}', Context.currentPos()); } } /** * Build manifest JSON files for each library */ private static function buildManifests(manifestDir:String) { // Create an array to hold all assets for the default manifest var allAssets = []; // First, process individual library manifests for (library in assetMap.keys()) { var assets = assetMap.get(library); var manifest = { name: library, assets: assets, rootPath: "../assets/" + library + "/" }; // Add assets to combined collection with library prefix for (asset in assets) { allAssets.push({ id: library + "/" + asset.id, path: library + "/" + asset.path, size: asset.size, type: asset.type }); } var manifestPath = manifestDir + "/" + library + ".json"; try { var content = Json.stringify(manifest, null, " "); File.saveContent(manifestPath, content); Context.info('Built manifest: ${manifestPath} with ${assets.length} assets', Context.currentPos()); } catch (e) { Context.warning('Failed to write manifest: ${manifestPath}. Error: ${e}', Context.currentPos()); } } // Create the default manifest with all assets var defaultManifest = { name: "default", assets: allAssets, rootPath: "../assets/" }; // Save the default manifest var defaultManifestPath = manifestDir + "/default.json"; try { var content = Json.stringify(defaultManifest, null, " "); File.saveContent(defaultManifestPath, content); Context.info('Built default manifest: ${defaultManifestPath} with ${allAssets.length} assets', Context.currentPos()); } catch (e) { Context.warning('Failed to write default manifest: ${defaultManifestPath}. Error: ${e}', Context.currentPos()); } } // Helper functions private static function getOutputDir():String { // Try to parse the --cpp argument from the build.hxml var args = Sys.args(); for (i in 0...args.length) { if (args[i] == "--cpp" && i < args.length - 1) { return args[i + 1]; } } return "bin/cpp"; // Default output directory } private static function createDirectory(path:String) { if (!FileSystem.exists(path)) { try { FileSystem.createDirectory(path); } catch (e) { Context.warning('Failed to create directory: ${path}. Error: ${e}', Context.currentPos()); } } } private static function removeDirectory(path:String):Void { if (!FileSystem.exists(path)) return; for (entry in FileSystem.readDirectory(path)) { var entryPath = path + "/" + entry; if (FileSystem.isDirectory(entryPath)) { removeDirectory(entryPath); } else { FileSystem.deleteFile(entryPath); } } FileSystem.deleteDirectory(path); } /** Stage the Lime CFFI module that matches the selected haxelib version. */ private static function copyLimeNativeLibrary(outputDir:String):Void { var platformDirectory = if (Context.defined("linux")) { "Linux64"; } else if (Context.defined("windows")) { "Windows64"; } else if (Context.defined("mac")) { Context.defined("arm64") ? "MacArm64" : "Mac64"; } else { return; } var limeRoot:String = null; for (classPath in Context.getClassPath()) { var normalized = classPath.split("\\").join("/"); if (normalized.indexOf("/lime/") != -1 && normalized.endsWith("/src/")) { limeRoot = normalized.substr(0, normalized.length - 4); break; } } if (limeRoot == null) { Context.warning("Could not locate Lime native library from the compiler class path", Context.currentPos()); return; } var source = limeRoot + "ndll/" + platformDirectory + "/lime.ndll"; if (!FileSystem.exists(source) && platformDirectory.endsWith("64")) { platformDirectory = platformDirectory.substr(0, platformDirectory.length - 2); source = limeRoot + "ndll/" + platformDirectory + "/lime.ndll"; } if (!FileSystem.exists(source)) { Context.warning("Lime native library not found: " + source, Context.currentPos()); return; } try { File.copy(source, outputDir + "/lime.ndll"); } catch (e:Dynamic) { Context.error("Failed to copy Lime native library: " + e, Context.currentPos()); } } private static function findAssetsMeta(metaAccess:Array):MetadataEntry { if (metaAccess == null) return null; for (meta in metaAccess) { if (meta.name == ":Assets" || meta.name == "Assets") { return meta; } } return null; } private static function extractString(expr:Expr):String { switch (expr.expr) { case EConst(CString(s)): return s; default: Context.error("Expected string literal", expr.pos); } return null; } private static function getAssetType(path:String):String { var ext = path.split(".").pop().toLowerCase(); return (switch (ext) { case "jpg", "jpeg", "png", "gif", "bmp": "image"; case "mp3", "ogg", "wav": "sound"; case "ttf", "otf": "font"; case "json", "xml", "txt": "text"; default: "binary"; }).toUpperCase(); } } #end