diff --git a/Builder/Beyond.NET.Builder.Android/AndroidBuilder.cs b/Builder/Beyond.NET.Builder.Android/AndroidBuilder.cs new file mode 100644 index 00000000..bc313f68 --- /dev/null +++ b/Builder/Beyond.NET.Builder.Android/AndroidBuilder.cs @@ -0,0 +1,117 @@ +using Beyond.NET.Core; + +namespace Beyond.NET.Builder.Android; + +public class AndroidBuilder +{ + public record BuildResult( + string? AndroidARM64LibraryPath, + string OutputDirectoryPath + ); + + public string ProductName { get; } + public string OutputDirectory { get; } + public bool BuildAndroidARM64 { get; } + + private ILogger Logger => Services.Shared.LoggerService; + + private string OutputProductName => ProductName; + private string OutputLibraryFileName => $"lib{OutputProductName}.so"; + + public AndroidBuilder( + string productName, + string outputDirectory, + bool buildAndroidARM64 + ) + { + ProductName = productName; + OutputDirectory = outputDirectory; + BuildAndroidARM64 = buildAndroidARM64; + } + + public BuildResult Build( + string? androidARM64BuildPath + ) + { + Logger.LogInformation("Building Android libraries"); + + string? androidARM64LibraryPath = null; + + if (androidARM64BuildPath is not null && BuildAndroidARM64) + { + androidARM64LibraryPath = Path.Combine(androidARM64BuildPath, OutputLibraryFileName); + + if (!File.Exists(androidARM64LibraryPath)) + { + var libraryWithoutLibName = OutputLibraryFileName.Replace("lib", ""); + androidARM64LibraryPath = Path.Combine(androidARM64BuildPath, libraryWithoutLibName); + } + + if (!File.Exists(androidARM64LibraryPath)) + { + throw new FileNotFoundException($"Android library not found at {androidARM64BuildPath}"); + } + + Logger.LogInformation($"Android library found at: {androidARM64LibraryPath}"); + } + + // Create output directory structure for Android + string androidOutputPath = Path.Combine(OutputDirectory, "android"); + Directory.CreateDirectory(androidOutputPath); + + if (androidARM64LibraryPath is not null) + { + string arm64OutputDir = Path.Combine(androidOutputPath, "arm64-v8a"); + Directory.CreateDirectory(arm64OutputDir); + string destPath = Path.Combine(arm64OutputDir, OutputLibraryFileName); + File.Copy(androidARM64LibraryPath, destPath, true); + Logger.LogInformation($"Copied library to: {destPath}"); + } + + if (androidARM64BuildPath is not null && BuildAndroidARM64) + { + string debugSymbolsOutputPath = Path.Combine(androidOutputPath, "android-debug-symbols"); + Directory.CreateDirectory(debugSymbolsOutputPath); + + Logger.LogInformation($"Copying all files from {androidARM64BuildPath} to {debugSymbolsOutputPath}"); + CopyAllFiles(androidARM64BuildPath, debugSymbolsOutputPath); + + Logger.LogInformation($"Copied debug symbols to: {debugSymbolsOutputPath}"); + } + + Logger.LogInformation($"Android build completed. Output directory: {androidOutputPath}"); + + return new BuildResult( + androidARM64LibraryPath, + androidOutputPath + ); + } + + private void CopyAllFiles(string sourceDirectory, string destinationDirectory) + { + if (!Directory.Exists(sourceDirectory)) + { + throw new DirectoryNotFoundException($"Source directory not found: {sourceDirectory}"); + } + + Directory.CreateDirectory(destinationDirectory); + + foreach (string filePath in Directory.GetFiles(sourceDirectory)) + { + string fileName = Path.GetFileName(filePath); + string destFilePath = Path.Combine(destinationDirectory, fileName); + + File.Copy(filePath, destFilePath, overwrite: true); + Logger.LogDebug($"Copied: {fileName}"); + } + + foreach (string dirPath in Directory.GetDirectories(sourceDirectory)) + { + string dirName = Path.GetFileName(dirPath); + string destDirPath = Path.Combine(destinationDirectory, dirName); + + CopyAllFiles(dirPath, destDirPath); + } + } +} + diff --git a/Builder/Beyond.NET.Builder.Android/AndroidPublish.cs b/Builder/Beyond.NET.Builder.Android/AndroidPublish.cs new file mode 100644 index 00000000..22cd5cf5 --- /dev/null +++ b/Builder/Beyond.NET.Builder.Android/AndroidPublish.cs @@ -0,0 +1,56 @@ +using Beyond.NET.Core; + +namespace Beyond.NET.Builder.Android; + +public static class AndroidPublish +{ + private const string BUILD_SCRIPT_NAME = "build_android.sh"; + + public static string Run( + string workingDirectory, + string runtimeIdentifier, + string configuration, + string? verbosityLevel + ) + { + // Get the path to the build script + string scriptDirectory = Path.GetDirectoryName(typeof(AndroidPublish).Assembly.Location)!; + string scriptPath = Path.Combine(scriptDirectory, BUILD_SCRIPT_NAME); + + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException($"Android build script not found at: {scriptPath}"); + } + + // Build arguments for the script + List args = new() + { + scriptPath, + workingDirectory, + runtimeIdentifier, + configuration + }; + + if (!string.IsNullOrEmpty(verbosityLevel)) + { + args.Add(verbosityLevel); + } + + // Execute the build script using bash + var bashApp = new CLIApp("/bin/bash"); + var result = bashApp.Launch( + args.ToArray(), + workingDirectory + ); + + Exception? failure = result.FailureAsException; + + if (failure is not null) + { + throw failure; + } + + return result.StandardOut ?? string.Empty; + } +} + diff --git a/Builder/Beyond.NET.Builder.Android/Beyond.NET.Builder.Android.csproj b/Builder/Beyond.NET.Builder.Android/Beyond.NET.Builder.Android.csproj new file mode 100644 index 00000000..31e74b57 --- /dev/null +++ b/Builder/Beyond.NET.Builder.Android/Beyond.NET.Builder.Android.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/Builder/Beyond.NET.Builder.Android/build_android.sh b/Builder/Beyond.NET.Builder.Android/build_android.sh new file mode 100755 index 00000000..523865ec --- /dev/null +++ b/Builder/Beyond.NET.Builder.Android/build_android.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# Script to build .NET NativeAOT for Android +# Usage: build_android.sh + +set -e + +# Parse arguments +WORKING_DIR="$1" +RUNTIME_IDENTIFIER="${2:-linux-bionic-arm64}" +CONFIGURATION="${3:-Release}" +VERBOSITY="${4:-normal}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Script is located at: $SCRIPT_DIR" + +if [ -z "$WORKING_DIR" ]; then + echo "Error: Working directory is required" + echo "Usage: $0 [runtime_identifier] [configuration] [verbosity]" + exit 1 +fi + +if [ ! -d "$WORKING_DIR" ]; then + echo "Error: Working directory does not exist: $WORKING_DIR" + exit 1 +fi + +echo "Building .NET NativeAOT for Android" +echo " Working Directory: $WORKING_DIR" +echo " Runtime Identifier: $RUNTIME_IDENTIFIER" +echo " Configuration: $CONFIGURATION" +echo " Verbosity: $VERBOSITY" +echo "" + +# Either set the "ANDROID_NDK_BIN_PATH" environment variable and point it to the following Android NDK installation directory's subpath "ndk_home/toolchains/llvm/prebuilt/platform-arch/bin" +# Or alternatively, make sure the "ANDROID_NDK_HOME" enviroment variable is set and pointing to your Android NDK installation. We'll then try to automatically detect where the bin path is. +if [ ! -d "${ANDROID_NDK_BIN_PATH}" ] ; then + echo "Warning: ANDROID_NDK_BIN_PATH enviroment variable is not set or directory does not exist. Trying to fall back to ANDROID_NDK_HOME and automatically detecting the bin directory." + + if [ ! -d "${ANDROID_NDK_HOME}" ] ; then + echo "Error: ANDROID_NDK_HOME enviroment variable is not set or directory does not exist." + exit 1 + fi + + echo "Android NDK Home: ${ANDROID_NDK_HOME}" + + # ndk_bin_common.sh sets the "HOST_TAG" environment variable to the NDK's toolchain platform + source "${ANDROID_NDK_HOME}/build/tools/ndk_bin_common.sh" + echo "Android Host Tag: ${HOST_TAG}" + + ANDROID_NDK_BIN_PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${HOST_TAG}/bin" + + if [ ! -d "${ANDROID_NDK_BIN_PATH}" ] ; then + echo "Error: Android NDK bin Path not found at ${ANDROID_NDK_BIN_PATH}" + exit 1 + fi +fi + +echo "Android NDK bin Path: ${ANDROID_NDK_BIN_PATH}" + +export PATH=${ANDROID_NDK_BIN_PATH}:$PATH + +echo "" + +# Change to working directory +cd "$WORKING_DIR" + +# Run dotnet publish with Android-specific settings +echo "Running: dotnet publish -r $RUNTIME_IDENTIFIER -c $CONFIGURATION -v $VERBOSITY -p:PublishAotUsingRuntimePack=true" +echo "" + +dotnet publish \ + -r "$RUNTIME_IDENTIFIER" \ + -c "$CONFIGURATION" \ + -v "$VERBOSITY" \ + -p:PublishAotUsingRuntimePack=true + +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 0 ]; then + echo "" + echo "Android build completed successfully" +else + echo "" + echo "Android build failed with exit code $EXIT_CODE" + exit $EXIT_CODE +fi + +exit 0 diff --git a/Builder/Beyond.NET.Builder.DotNET/PlatformIdentifier.cs b/Builder/Beyond.NET.Builder.DotNET/PlatformIdentifier.cs index bc60b9f4..f87992d3 100644 --- a/Builder/Beyond.NET.Builder.DotNET/PlatformIdentifier.cs +++ b/Builder/Beyond.NET.Builder.DotNET/PlatformIdentifier.cs @@ -10,4 +10,6 @@ public static class PlatformIdentifier /// Made up, not part of .NET! /// public const string Apple = "apple"; + + public const string Android = "linux-bionic"; } \ No newline at end of file diff --git a/Builder/Beyond.NET.Builder.DotNET/RuntimeIdentifier.cs b/Builder/Beyond.NET.Builder.DotNET/RuntimeIdentifier.cs index 1e8acb05..9b1e2a5c 100644 --- a/Builder/Beyond.NET.Builder.DotNET/RuntimeIdentifier.cs +++ b/Builder/Beyond.NET.Builder.DotNET/RuntimeIdentifier.cs @@ -28,4 +28,6 @@ public static class RuntimeIdentifier /// Made up, not part of .NET! /// public const string APPLE_UNIVERSAL = $"{PlatformIdentifier.Apple}-{TargetIdentifier.UNIVERSAL}"; + + public const string Android_ARM64 = $"{PlatformIdentifier.Android}-{TargetIdentifier.ARM64}"; } \ No newline at end of file diff --git a/Builder/Beyond.NET.Builder/Beyond.NET.Builder.csproj b/Builder/Beyond.NET.Builder/Beyond.NET.Builder.csproj index fd74b237..02a9b746 100644 --- a/Builder/Beyond.NET.Builder/Beyond.NET.Builder.csproj +++ b/Builder/Beyond.NET.Builder/Beyond.NET.Builder.csproj @@ -9,6 +9,7 @@ + diff --git a/Builder/Beyond.NET.Builder/BuildTargets.cs b/Builder/Beyond.NET.Builder/BuildTargets.cs index 0b3f4d0e..7350faeb 100644 --- a/Builder/Beyond.NET.Builder/BuildTargets.cs +++ b/Builder/Beyond.NET.Builder/BuildTargets.cs @@ -10,6 +10,7 @@ public enum BuildTargets iOSARM64 = 1 << 2, iOSSimulatorARM64 = 1 << 3, iOSSimulatorX64 = 1 << 4, + AndroidARM64 = 1 << 5, MacOSUniversal = MacOSARM64 | MacOSX64, iOSSimulatorUniversal = iOSSimulatorARM64 | iOSSimulatorX64, @@ -38,4 +39,9 @@ public static bool ContainsAnyMacOSTarget(this BuildTargets buildTargets) return buildTargets.HasFlag(BuildTargets.MacOSARM64) || buildTargets.HasFlag(BuildTargets.MacOSX64); } + + public static bool ContainsAnyAndroidTarget(this BuildTargets buildTargets) + { + return buildTargets.HasFlag(BuildTargets.AndroidARM64); + } } \ No newline at end of file diff --git a/Builder/Beyond.NET.Builder/DotNETNativeBuilder.cs b/Builder/Beyond.NET.Builder/DotNETNativeBuilder.cs index 60b8aadf..46a25e69 100644 --- a/Builder/Beyond.NET.Builder/DotNETNativeBuilder.cs +++ b/Builder/Beyond.NET.Builder/DotNETNativeBuilder.cs @@ -1,3 +1,4 @@ +using Beyond.NET.Builder.Android; using Beyond.NET.Builder.Apple.Framework; using Beyond.NET.Builder.Apple.XCRun; using Beyond.NET.Builder.DotNET; @@ -127,7 +128,17 @@ public BuildResult Build() File.Copy(GeneratedCSharpFilePath, generatedCSharpDestinationFilePath); #endregion Copy Material to Temp Dir - if (swiftBuildResult is not null) { + bool hasAppleTargets = swiftBuildResult is not null; + bool hasAndroidTargets = Targets.ContainsAnyAndroidTarget(); + + if (!hasAppleTargets && !hasAndroidTargets) { + throw new Exception("No valid build targets specified. Either Swift build result for Apple targets or Android targets must be provided."); + } + + string? appleOutputPath = null; + string? androidOutputPath = null; + + if (hasAppleTargets) { #region Build for Apple #region Prepare File Paths const string binDirName = "bin"; @@ -460,18 +471,101 @@ public BuildResult Build() appleUniversalXCFrameworkFilePath! ); - string outputDirectoryPath = appleUniversalBuildPath!; + appleOutputPath = appleUniversalBuildPath!; #endregion Apple Universal + #endregion Build for Apple + } + + if (hasAndroidTargets) { + #region Build for Android + #region Prepare File Paths + const string binDirName = "bin"; + const string publishDirName = "publish"; + + string? androidARM64BuildDir = Targets.HasFlag(BuildTargets.AndroidARM64) ? $"{binDirName}/{BUILD_CONFIGURATION}/{TargetFramework}/{RuntimeIdentifier.Android_ARM64}/{publishDirName}" : null; + string androidBuildDir = $"{binDirName}/{BUILD_CONFIGURATION}/{TargetFramework}/{RuntimeIdentifier.Android_ARM64}/{publishDirName}"; + + string? androidARM64TempPath = null; + string? androidARM64BuildPath = null; + + if (Targets.HasFlag(BuildTargets.AndroidARM64)) { + androidARM64TempPath = Directory.CreateTempSubdirectory(tempDirectoryPrefix + "_Android_ARM64").FullName; + tempDirPaths.Add(androidARM64TempPath); + Logger.LogDebug($"Created temp directory for Android ARM64 build at \"{androidARM64TempPath}\""); + + FileSystemUtils.CopyDirectoryContents(tempDirectoryPath, androidARM64TempPath, true); + androidARM64BuildPath = Path.Combine(androidARM64TempPath, androidARM64BuildDir!); + } + + string androidTempPath = Directory.CreateTempSubdirectory(tempDirectoryPrefix + "_Android").FullName; + tempDirPaths.Add(androidTempPath); + Logger.LogDebug($"Created temp directory for Android build at \"{androidTempPath}\""); + string androidBuildPath = Path.Combine(androidTempPath, androidBuildDir); + #endregion Prepare File Paths + + #region dotnet publish + if (Targets.HasFlag(BuildTargets.AndroidARM64)) { + AndroidPublish(androidARM64TempPath!, RuntimeIdentifier.Android_ARM64); + } + #endregion dotnet publish + + #region Build Android Libraries + Directory.CreateDirectory(androidBuildPath); - return new( - tempDirPaths, - outputDirectoryPath + var androidBuilder = new Android.AndroidBuilder( + ProductName, + androidBuildPath, + Targets.HasFlag(BuildTargets.AndroidARM64) ); - #endregion Build for Apple + + var androidBuildResult = androidBuilder.Build( + androidARM64BuildPath + ); + + androidOutputPath = androidBuildResult.OutputDirectoryPath; + #endregion Build Android Libraries + #endregion Build for Android + } + + #region Combine Outputs + string finalOutputPath; + + if (hasAppleTargets && hasAndroidTargets) { + // Both platforms: create a combined output directory + string combinedOutputTempPath = Directory.CreateTempSubdirectory(tempDirectoryPrefix + "_Combined_Output").FullName; + tempDirPaths.Add(combinedOutputTempPath); + Logger.LogDebug($"Created temp directory for combined output at \"{combinedOutputTempPath}\""); + + // Copy Apple output + if (appleOutputPath is not null) { + string appleDestPath = Path.Combine(combinedOutputTempPath, "apple"); + Directory.CreateDirectory(appleDestPath); + FileSystemUtils.CopyDirectoryContents(appleOutputPath, appleDestPath, true); + Logger.LogDebug($"Copied Apple output to \"{appleDestPath}\""); + } + + // Copy Android output + if (androidOutputPath is not null) { + string androidDestPath = Path.Combine(combinedOutputTempPath, "android"); + Directory.CreateDirectory(androidDestPath); + FileSystemUtils.CopyDirectoryContents(androidOutputPath, androidDestPath, true); + Logger.LogDebug($"Copied Android output to \"{androidDestPath}\""); + } + + finalOutputPath = combinedOutputTempPath; + } else if (hasAppleTargets) { + // Apple only + finalOutputPath = appleOutputPath!; } else { - // TODO: Currently only apple builds are supported - throw new Exception("No swift build result"); + // Android only + finalOutputPath = androidOutputPath!; } + #endregion Combine Outputs + + return new( + tempDirPaths, + finalOutputPath + ); } private void DotNETPublish( @@ -489,6 +583,21 @@ string runtimeIdentifier ); } + private void AndroidPublish( + string workingDirectory, + string runtimeIdentifier + ) + { + Logger.LogInformation($"Compiling .NET NativeAOT project for Android in \"{workingDirectory}\" with runtime identifier \"{runtimeIdentifier}\""); + + Android.AndroidPublish.Run( + workingDirectory, + runtimeIdentifier, + BUILD_CONFIGURATION, + VERBOSITY_LEVEL + ); + } + private void CreateSwiftModule( SwiftBuilder.PartialCompileResult partialCompileResult, string targetSwiftModuleDirPath diff --git a/Generator/Beyond.NET.CodeGenerator.CLI/Source/BuildConfiguration.cs b/Generator/Beyond.NET.CodeGenerator.CLI/Source/BuildConfiguration.cs index 3374fad0..fc8b3571 100644 --- a/Generator/Beyond.NET.CodeGenerator.CLI/Source/BuildConfiguration.cs +++ b/Generator/Beyond.NET.CodeGenerator.CLI/Source/BuildConfiguration.cs @@ -2,7 +2,8 @@ namespace Beyond.NET.CodeGenerator.CLI; public record BuildConfiguration ( - string Target, + string? Target, + string[]? Targets, string? ProductName, string? ProductBundleIdentifier, @@ -15,13 +16,35 @@ public record BuildConfiguration bool DisableStripDotNETSymbols, string[]? NoWarn -); +) +{ + /// + /// Gets all build targets, supporting both single Target and multiple Targets properties. + /// + public string[] GetAllTargets() + { + var targets = new List(); + + if (!string.IsNullOrEmpty(Target)) + { + targets.Add(Target); + } + + if (Targets != null && Targets.Length > 0) + { + targets.AddRange(Targets); + } + + return targets.Distinct().ToArray(); + } +}; internal static class BuildTargets { public const string APPLE_UNIVERSAL = "apple-universal"; public const string MACOS_UNIVERSAL = "macos-universal"; public const string IOS_UNIVERSAL = "ios-universal"; + public const string ANDROID_ARM64 = "android-arm64"; } internal static class AppleDeploymentTargets diff --git a/Generator/Beyond.NET.CodeGenerator.CLI/Source/CodeGeneratorDriver.cs b/Generator/Beyond.NET.CodeGenerator.CLI/Source/CodeGeneratorDriver.cs index 5b9106c0..1861f979 100644 --- a/Generator/Beyond.NET.CodeGenerator.CLI/Source/CodeGeneratorDriver.cs +++ b/Generator/Beyond.NET.CodeGenerator.CLI/Source/CodeGeneratorDriver.cs @@ -70,7 +70,7 @@ internal void Generate() BuildConfiguration? buildConfig = Configuration.Build; bool buildEnabled = false; - string? buildTarget = null; + string[]? buildTargets = null; string? buildProductName = null; string? buildProductBundleIdentifier = null; string? buildProductOutputPath = null; @@ -82,12 +82,20 @@ internal void Generate() if (buildConfig is not null) { buildEnabled = true; - buildTarget = buildConfig.Target; + buildTargets = buildConfig.GetAllTargets(); - if (buildTarget != BuildTargets.APPLE_UNIVERSAL && - buildTarget != BuildTargets.MACOS_UNIVERSAL && - buildTarget != BuildTargets.IOS_UNIVERSAL) { - throw new Exception($"Only \"{BuildTargets.APPLE_UNIVERSAL}\", \"{BuildTargets.MACOS_UNIVERSAL}\" and \"{BuildTargets.IOS_UNIVERSAL}\" are currently supported as \"{nameof(buildConfig.Target)}\""); + if (buildTargets.Length == 0) { + throw new Exception($"At least one build target must be specified in \"{nameof(buildConfig.Target)}\" or \"{nameof(buildConfig.Targets)}\""); + } + + // Validate all targets + foreach (var target in buildTargets) { + if (target != BuildTargets.APPLE_UNIVERSAL && + target != BuildTargets.MACOS_UNIVERSAL && + target != BuildTargets.IOS_UNIVERSAL && + target != BuildTargets.ANDROID_ARM64) { + throw new Exception($"Build target \"{target}\" is not supported. Supported targets: \"{BuildTargets.APPLE_UNIVERSAL}\", \"{BuildTargets.MACOS_UNIVERSAL}\", \"{BuildTargets.IOS_UNIVERSAL}\", \"{BuildTargets.ANDROID_ARM64}\""); + } } var potentialProductName = buildConfig.ProductName; @@ -419,55 +427,95 @@ out Dictionary unsupportedTypes #region Build if (buildEnabled) { - // TODO: This assumes that we're always building for Apple platforms - if (string.IsNullOrEmpty(buildProductName) || - string.IsNullOrEmpty(buildProductBundleIdentifier) || - string.IsNullOrEmpty(buildProductOutputPath) || - string.IsNullOrEmpty(cSharpUnmanagedOutputPath) || - string.IsNullOrEmpty(cOutputPath) || - string.IsNullOrEmpty(swiftOutputPath) || - string.IsNullOrEmpty(buildMacOSDeploymentTarget) || - string.IsNullOrEmpty(buildiOSDeploymentTarget)) { - // We checked all of these above so we shouldn't get here but just in case... - throw new Exception("Invalid build configuration"); + // Determine which platforms are being targeted + bool hasAndroidTargets = buildTargets!.Any(t => + t == BuildTargets.ANDROID_ARM64); + + bool hasAppleTargets = buildTargets!.Any(t => + t == BuildTargets.APPLE_UNIVERSAL || + t == BuildTargets.MACOS_UNIVERSAL || + t == BuildTargets.IOS_UNIVERSAL); + + // Validate required configuration for each platform + if (hasAppleTargets) { + // Apple builds require Swift + if (string.IsNullOrEmpty(buildProductName) || + string.IsNullOrEmpty(buildProductBundleIdentifier) || + string.IsNullOrEmpty(buildProductOutputPath) || + string.IsNullOrEmpty(cSharpUnmanagedOutputPath) || + string.IsNullOrEmpty(cOutputPath) || + string.IsNullOrEmpty(swiftOutputPath) || + string.IsNullOrEmpty(buildMacOSDeploymentTarget) || + string.IsNullOrEmpty(buildiOSDeploymentTarget)) { + throw new Exception("Invalid build configuration for Apple platforms"); + } } - Beyond.NET.Builder.BuildTargets builderBuildTargets; + if (hasAndroidTargets) { + // Android builds don't require Swift but need basic config + if (string.IsNullOrEmpty(buildProductName) || + string.IsNullOrEmpty(buildProductOutputPath) || + string.IsNullOrEmpty(cSharpUnmanagedOutputPath) || + string.IsNullOrEmpty(cOutputPath)) { + throw new Exception("Invalid build configuration for Android platforms"); + } + } - if (buildTarget == BuildTargets.APPLE_UNIVERSAL) { - builderBuildTargets = Beyond.NET.Builder.BuildTargets.AppleUniversal; - } else if (buildTarget == BuildTargets.MACOS_UNIVERSAL) { - builderBuildTargets = Beyond.NET.Builder.BuildTargets.MacOSUniversal; - } else if (buildTarget == BuildTargets.IOS_UNIVERSAL) { - builderBuildTargets = Beyond.NET.Builder.BuildTargets.iOSUniversal; - } else { - throw new Exception($"Build Target \"{buildTarget}\" is not a supported target for the SwiftBuilder"); + // Convert all targets to builder flags + Beyond.NET.Builder.BuildTargets builderBuildTargets = Beyond.NET.Builder.BuildTargets.None; + + foreach (var target in buildTargets!) { + if (target == BuildTargets.APPLE_UNIVERSAL) { + builderBuildTargets |= Beyond.NET.Builder.BuildTargets.AppleUniversal; + } else if (target == BuildTargets.MACOS_UNIVERSAL) { + builderBuildTargets |= Beyond.NET.Builder.BuildTargets.MacOSUniversal; + } else if (target == BuildTargets.IOS_UNIVERSAL) { + builderBuildTargets |= Beyond.NET.Builder.BuildTargets.iOSUniversal; + } else if (target == BuildTargets.ANDROID_ARM64) { + builderBuildTargets |= Beyond.NET.Builder.BuildTargets.AndroidARM64; + } } - SwiftBuilder.BuilderConfiguration config = new( - builderBuildTargets, - buildProductName, - buildProductBundleIdentifier, - cOutputPath, - swiftOutputPath, - buildMacOSDeploymentTarget, - buildiOSDeploymentTarget, - !disableParallelBuild - ); + if (builderBuildTargets == Beyond.NET.Builder.BuildTargets.None) { + throw new Exception("No valid build targets were specified"); + } - Logger.LogInformation("Building Swift bindings"); + Logger.LogInformation($"Building for targets: {string.Join(", ", buildTargets!)}"); - SwiftBuilder swiftBuilder = new(config); + SwiftBuilder.BuildResult? swiftBuildResult = null; - var swiftBuildResult = swiftBuilder.Build(); + if (hasAppleTargets) { + // Extract only Apple targets for Swift builder + Beyond.NET.Builder.BuildTargets appleTargets = builderBuildTargets & + (Beyond.NET.Builder.BuildTargets.AppleUniversal | + Beyond.NET.Builder.BuildTargets.MacOSUniversal | + Beyond.NET.Builder.BuildTargets.iOSUniversal); - if (!Directory.Exists(swiftBuildResult.OutputRootPath)) { - throw new Exception($"Swift product directory does not exist at \"{swiftBuildResult.OutputRootPath}\""); - } + SwiftBuilder.BuilderConfiguration config = new( + appleTargets, + buildProductName!, + buildProductBundleIdentifier!, + cOutputPath!, + swiftOutputPath!, + buildMacOSDeploymentTarget!, + buildiOSDeploymentTarget!, + !disableParallelBuild + ); + + Logger.LogInformation("Building Swift bindings"); + + SwiftBuilder swiftBuilder = new(config); - tempDirPaths.Add(swiftBuildResult.OutputRootPath); + swiftBuildResult = swiftBuilder.Build(); - Logger.LogInformation($"Swift bindings built at \"{swiftBuildResult.OutputRootPath}\""); + if (!Directory.Exists(swiftBuildResult.OutputRootPath)) { + throw new Exception($"Swift product directory does not exist at \"{swiftBuildResult.OutputRootPath}\""); + } + + tempDirPaths.Add(swiftBuildResult.OutputRootPath); + + Logger.LogInformation($"Swift bindings built at \"{swiftBuildResult.OutputRootPath}\""); + } Logger.LogInformation("Building .NET Native stuff"); @@ -478,13 +526,13 @@ out Dictionary unsupportedTypes var dnNativeBuilder = new DotNETNativeBuilder( builderBuildTargets, dotNetTargetFramework, - buildProductName, - buildProductBundleIdentifier, + buildProductName!, + buildProductBundleIdentifier ?? string.Empty, assemblyPath, assemblyReferences, Configuration.Build?.NoWarn ?? [], !disableStripDotNETSymbols, - cSharpUnmanagedOutputPath, + cSharpUnmanagedOutputPath!, !disableParallelBuild, swiftBuildResult ); diff --git a/Generator/Beyond.NET.CodeGenerator.CLI/publish_android b/Generator/Beyond.NET.CodeGenerator.CLI/publish_android new file mode 100755 index 00000000..c706315b --- /dev/null +++ b/Generator/Beyond.NET.CodeGenerator.CLI/publish_android @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +# Script to build Beyond.NET Code Generator for Android platforms +# See https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/android-bionic.md for instructions + +set -e + +# Either set the "ANDROID_NDK_BIN_PATH" environment variable and point it to the following Android NDK installation directory's subpath "ndk_home/toolchains/llvm/prebuilt/platform-arch/bin" +# Or alternatively, make sure the "ANDROID_NDK_HOME" enviroment variable is set and pointing to your Android NDK installation. We'll then try to automatically detect where the bin path is. +if [ ! -d "${ANDROID_NDK_BIN_PATH}" ] ; then + echo "Warning: ANDROID_NDK_BIN_PATH enviroment variable is not set or directory does not exist. Trying to fall back to ANDROID_NDK_HOME and automatically detecting the bin directory." + + if [ ! -d "${ANDROID_NDK_HOME}" ] ; then + echo "Error: ANDROID_NDK_HOME enviroment variable is not set or directory does not exist." + exit 1 + fi + + echo "Android NDK Home: ${ANDROID_NDK_HOME}" + + # ndk_bin_common.sh sets the "HOST_TAG" environment variable to the NDK's toolchain platform + source "${ANDROID_NDK_HOME}/build/tools/ndk_bin_common.sh" + echo "Android Host Tag: ${HOST_TAG}" + + ANDROID_NDK_BIN_PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${HOST_TAG}/bin" + + if [ ! -d "${ANDROID_NDK_BIN_PATH}" ] ; then + echo "Error: Android NDK bin Path not found at ${ANDROID_NDK_BIN_PATH}" + exit 1 + fi +fi + +echo "Android NDK bin Path: ${ANDROID_NDK_BIN_PATH}" + +export PATH=${ANDROID_NDK_BIN_PATH}:$PATH + +echo "" + +OUTPUT_PRODUCT_NAME="beyondnetgen" +BUILD_CONFIGURATION="Release" + +OUTPUT_FILE_NAME="${OUTPUT_PRODUCT_NAME}" + +VERBOSITY_LEVEL="normal" + +RUNTIME_IDENTIFIER_ARM64="linux-bionic-arm64" + +BIN_DIR="bin" +PUBLISH_DIR="publish" + +ARM64_BUILD_DIR="${BIN_DIR}/${BUILD_CONFIGURATION}/${RUNTIME_IDENTIFIER_ARM64}/${PUBLISH_DIR}" + +ARM64_FILE_PATH="${ARM64_BUILD_DIR}/${OUTPUT_FILE_NAME}" + +echo "Cleaning ${OUTPUT_PRODUCT_NAME}" +dotnet clean /p:Configuration=Release + +echo "Building ${OUTPUT_PRODUCT_NAME} for Android" +dotnet publish \ + -r ${RUNTIME_IDENTIFIER_ARM64} \ + -v "${VERBOSITY_LEVEL}" \ + -p:PublishAotUsingRuntimePack=true + +echo "Build complete!" +echo "ARM64 binary: ${ARM64_FILE_PATH}" + diff --git a/README.md b/README.md index fe80b21b..b83c2191 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Beyond.NET is a toolset that makes it possible to call .NET code from other programming languages. Conceptually, think of it like the reverse of the Xamarin tools. -Currently, C and Swift are the supported output languages. But any language that has C interoperability can use the generated bindings. +Currently, C, Swift, and Kotlin are the supported output languages. But any language that has C interoperability can use the generated bindings. @@ -28,6 +28,7 @@ The generated C# code can then be compiled with .NET NativeAOT which allows the - Make sure [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) is installed and on your path. - On macOS, make sure [Xcode](https://developer.apple.com/xcode/), the macOS and iOS SDKs and the Command Line Tools (`xcode-select --install`) are installed. - On Linux, make sure clang and zlib are installed +- For Android builds, make sure the [Android NDK](https://developer.android.com/ndk/downloads) is installed and configured (see [Android Build Guide](docs/ANDROID_BUILD.md)) ### Generator Executable @@ -50,10 +51,12 @@ The generated C# code can then be compiled with .NET NativeAOT which allows the ### Generator Modes -The generator always generates language bindings (C header file and optionally a Swift source code file) but it can also be configured to automatically compile a native version of the target assembly. -At the moment, automatic build support is only available on Apple platforms. +The generator always generates language bindings (C header file and optionally Swift and/or Kotlin source code files) but it can also be configured to automatically compile a native version of the target assembly. -If enabled, an [XCFramework](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle) containing compiled binaries for macOS ARM64, macOS x64, iOS ARM64, iOS Simulator ARM64 and iOS Simulator x64 is built. The generated XCFramework is ready to use and can just be dropped into an Xcode project. +Automatic build support is available for: +- **Apple platforms**: Generates an [XCFramework](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle) containing compiled binaries for macOS ARM64, macOS x64, iOS ARM64, iOS Simulator ARM64 and iOS Simulator x64. The generated XCFramework is ready to use and can just be dropped into an Xcode project. +- **Android platforms**: Generates native libraries (.so files) for ARM64 architecture in the Android jniLibs structure, ready to be integrated into Android projects. +- **Multi-platform**: Build for multiple platforms simultaneously (e.g., iOS + Android) in a single operation. See the [Multi-Platform Build Guide](docs/MULTI_PLATFORM_BUILD.md). We recommend using the automatic build support if possible. If you decide to [do things manually](README_MANUAL_BUILD.md), you will have to compile the generated C# file using NativeAOT, then link the resulting dynamic library into your native code and include the generated language bindings to call into it. @@ -181,6 +184,42 @@ struct ContentView: View { +### Creating a native version of a .NET classlib for Android + + +1. **Set up Android NDK**: + ```bash + export ANDROID_NDK_HOME="/path/to/android-ndk" + ``` + +2. **Create a configuration file** (`MyProject_Android_Config.json`): + ```json + { + "AssemblyPath": "path/to/YourAssembly.dll", + "Build": { + "Target": "android-arm64", + "ProductName": "YourLibraryName", + "ProductOutputPath": "output/path" + }, + "CSharpUnmanagedOutputPath": "Generated.cs", + "COutputPath": "Generated.h", + "KotlinOutputPath": "Generated.kt", + "KotlinPackageName": "com.yourcompany.yourapp", + "KotlinNativeLibraryName": "YourLibraryName" + } + ``` + +3. **Run the generator**: + ```bash + beyondnetgen MyProject_Android_Config.json + ``` + +4. **Copy the generated libraries** to your Android project: + ```bash + cp -r output/path/android/jniLibs app/src/main/ + ``` + + ## Generator Configuration The generator currently uses a configuration file where all of its options are specified. @@ -193,6 +232,7 @@ The generator currently uses a configuration file where all of its options are s "Build": { "Target": "apple-universal", + "Targets": ["ios-universal", "android-universal"], "ProductName": "AssemblyKit", "ProductBundleIdentifier": "com.mycompany.assemblykit", @@ -248,10 +288,11 @@ The generator currently uses a configuration file where all of its options are s - **`AssemblyPath`**: Enter the path to the compiled .NET assembly you want to generate native bindings for. (Required) - **`Build`**: Configuration options for automatic build support. (Optional; automatic build is disabled if not provided) - - **`Target`**: The platform and architecture to build for. (Required; currently `apple-universal`, `macos-universal` and `ios-universal` are supported) - - **`ProductName`**: The name of the resulting XCFramework and Swift/Clang module. This must be different than the target assembly name and any namespaces contained within it or its dependencies. (Optional; if not provided the assembly file name suffixed with `Kit` is used) - - **`ProductBundleIdentifier`**: The bundle identifier of the resulting frameworks. (Optional; if not provided the bundle identifier is `com.mycompany.` suffixed with the `ProductName`) - - **`ProductOutputPath`**: The output path for the resulting XCFramework. (Optional; if not provided, the directory of the `AssemblyPath` is used) + - **`Target`**: Single platform to build for. (Optional; use `Targets` for multiple platforms. Supported values: `apple-universal`, `macos-universal`, `ios-universal`, `android-arm64`) + - **`Targets`**: Array of platforms to build for simultaneously. (Optional; can be used with or instead of `Target`. See [Multi-Platform Build Guide](docs/MULTI_PLATFORM_BUILD.md)) + - **`ProductName`**: The name of the resulting libraries and modules. This must be different than the target assembly name and any namespaces contained within it or its dependencies. (Optional; if not provided the assembly file name suffixed with `Kit` is used) + - **`ProductBundleIdentifier`**: The bundle identifier of the resulting frameworks (Apple platforms only). (Optional; if not provided the bundle identifier is `com.mycompany.` suffixed with the `ProductName`) + - **`ProductOutputPath`**: The output path for the resulting libraries. When building for multiple platforms, outputs are organized in subdirectories. (Optional; if not provided, the directory of the `AssemblyPath` is used) - **`MacOSDeploymentTarget`**: The deployment target for the macOS portion of the XCFramework. (Optional; if not provided, `13.0` is used) - **`iOSDeploymentTarget`**: The deployment target for the iOS portion of the XCFramework. (Optional; if not provided, `16.0` is used) - **`DisableParallelBuild`**: Set to `true` to disable building in parallel (ie. for improved debugging). (Optional; if not provided, `false` is used) diff --git a/Samples/Beyond.NET.Sample_Android_Config.json b/Samples/Beyond.NET.Sample_Android_Config.json new file mode 100644 index 00000000..32f01f53 --- /dev/null +++ b/Samples/Beyond.NET.Sample_Android_Config.json @@ -0,0 +1,19 @@ +{ + "AssemblyPath": "Beyond.NET.Sample.Managed/bin/Release/Beyond.NET.Sample.Managed.dll", + "Build": { + "Target": "android-arm64", + "ProductName": "BeyondDotNETSample", + "ProductOutputPath": "Beyond.NET.Sample.Android/app/src/main/jniLibs" + }, + "CSharpUnmanagedOutputPath": "Beyond.NET.Sample.Native/Generated.cs", + "COutputPath": "Beyond.NET.Sample.C/Generated.h", + "KotlinOutputPath": "Beyond.NET.Sample.Android/app/src/main/java/com/example/beyondnetsampleandroid/Generated.kt", + "KotlinPackageName": "com.example.beyondnetsampleandroid", + "KotlinNativeLibraryName": "BeyondDotNETSample", + "EmitUnsupported": false, + "GenerateTypeCheckedDestroyMethods": true, + "EnableGenericsSupport": true, + "DoNotGenerateDocumentation": false, + "DoNotDeleteTemporaryDirectories": true +} + diff --git a/Samples/Beyond.NET.Sample_MultiPlatform_Config.json b/Samples/Beyond.NET.Sample_MultiPlatform_Config.json new file mode 100644 index 00000000..9c1c9b01 --- /dev/null +++ b/Samples/Beyond.NET.Sample_MultiPlatform_Config.json @@ -0,0 +1,25 @@ +{ + "AssemblyPath": "Beyond.NET.Sample.Managed/bin/Release/Beyond.NET.Sample.Managed.dll", + "Build": { + "Targets": [ + "ios-universal", + "android-arm64" + ], + "ProductName": "BeyondDotNETSample", + "ProductOutputPath": "Beyond.NET.Sample.MultiPlatform/output", + "MacOSDeploymentTarget": "13.0", + "iOSDeploymentTarget": "16.0" + }, + "CSharpUnmanagedOutputPath": "Beyond.NET.Sample.Native/Generated.cs", + "COutputPath": "Beyond.NET.Sample.C/Generated.h", + "SwiftOutputPath": "Beyond.NET.Sample.Swift/Generated.swift", + "KotlinOutputPath": "Beyond.NET.Sample.Android/app/src/main/java/com/example/beyondnetsampleandroid/Generated.kt", + "KotlinPackageName": "com.example.beyondnetsampleandroid", + "KotlinNativeLibraryName": "BeyondDotNETSample", + "EmitUnsupported": false, + "GenerateTypeCheckedDestroyMethods": true, + "EnableGenericsSupport": true, + "DoNotGenerateDocumentation": false, + "DoNotDeleteTemporaryDirectories": false +} +