diff --git a/.github/scripts/generate-quality-report.py b/.github/scripts/generate-quality-report.py index 9e57754a92..f3da9e9000 100755 --- a/.github/scripts/generate-quality-report.py +++ b/.github/scripts/generate-quality-report.py @@ -25,6 +25,8 @@ Path("maven/core-unittests/src/test/java"), ] +DEFAULT_REPORT_TITLE = "✅ Continuous Quality Report" + def _load_target_dirs() -> List[Path]: env_value = os.environ.get("QUALITY_REPORT_TARGET_DIRS") @@ -618,6 +620,7 @@ def build_report( html_urls: Dict[str, Optional[str]], coverage_html_url: Optional[str], coverage_archive_url: Optional[str], + title: str, ) -> str: if HTML_REPORT_DIR.exists(): for child in HTML_REPORT_DIR.iterdir(): @@ -650,7 +653,7 @@ def build_report( blob_base = f"{server_url.rstrip('/')}/{repository}/blob/{ref}" lines = [ - "## ✅ Continuous Quality Report", + f"## {title}", "", "### Test & Coverage", format_tests(tests), @@ -714,7 +717,14 @@ def main() -> None: coverage_html_url = os.environ.get("JACOCO_HTML_URL") coverage_archive_url = os.environ.get("JACOCO_REPORT_URL") generate_html_only = os.environ.get("QUALITY_REPORT_GENERATE_HTML_ONLY") == "1" - report = build_report(archive_urls, html_urls, coverage_html_url, coverage_archive_url) + report_title = os.environ.get("QUALITY_REPORT_TITLE") or DEFAULT_REPORT_TITLE + report = build_report( + archive_urls, + html_urls, + coverage_html_url, + coverage_archive_url, + report_title, + ) if not generate_html_only: REPORT_PATH.write_text(report + "\n", encoding="utf-8") diff --git a/.github/scripts/publish-quality-comment.js b/.github/scripts/publish-quality-comment.js new file mode 100644 index 0000000000..e14517d4e0 --- /dev/null +++ b/.github/scripts/publish-quality-comment.js @@ -0,0 +1,48 @@ +const fs = require('fs'); + +/** + * Publish or update a pull request comment containing a quality report. + * + * @param {{github: import('@actions/github').GitHub, context: any, core: any, marker?: string, reportPath?: string}} options + */ +async function publishQualityComment({ github, context, core, marker, reportPath }) { + const effectiveMarker = marker || ''; + const report = reportPath || 'quality-report.md'; + + if (!fs.existsSync(report)) { + core.warning(`${report} was not generated.`); + return; + } + + const body = `${effectiveMarker}\n${fs.readFileSync(report, 'utf8')}`; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + per_page: 100, + }); + + const existing = comments.find( + (comment) => comment.user?.type === 'Bot' && comment.body?.includes(effectiveMarker), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } +} + +module.exports = { publishQualityComment }; diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 700264a7d1..74d6461c5c 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -29,6 +29,85 @@ jobs: java-version: '8' cache: 'maven' + - name: Install native build tools + run: | + sudo apt-get update + sudo apt-get install -y clang + - name: Run ParparVM JVM tests working-directory: vm/tests run: mvn -B test + + - name: Publish ByteCodeTranslator quality previews + if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + id: publish-bytecode-quality-previews + env: + GITHUB_TOKEN: ${{ github.token }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + run: | + set -euo pipefail + if [ "${SERVER_URL}" != "https://github.com" ]; then + echo "HTML previews are only published for github.com instances." + exit 0 + fi + + coverage_dir="vm/tests/target/site/jacoco" + if [ ! -f "${coverage_dir}/index.html" ]; then + echo "No HTML outputs detected; skipping preview publishing." + exit 0 + fi + + run_dir="runs/${RUN_ID}-${RUN_ATTEMPT}" + tmp_dir=$(mktemp -d) + dest_dir="${tmp_dir}/${run_dir}/coverage" + mkdir -p "${dest_dir}" + cp -R "${coverage_dir}/." "${dest_dir}/" + + printf '%s\n%s\n%s\n' \ + '# Quality report previews' \ + '' \ + 'This branch is automatically managed by the PR CI workflow and may be force-pushed.' \ + > "${tmp_dir}/README.md" + + git -C "${tmp_dir}" init -b previews >/dev/null + git -C "${tmp_dir}" config user.name "github-actions[bot]" + git -C "${tmp_dir}" config user.email "github-actions[bot]@users.noreply.github.com" + git -C "${tmp_dir}" add . + git -C "${tmp_dir}" commit -m "Publish quality report previews for run ${RUN_ID} (attempt ${RUN_ATTEMPT})" >/dev/null + + remote_url="${SERVER_URL}/${REPOSITORY}.git" + token_remote_url="${remote_url/https:\/\//https://x-access-token:${GITHUB_TOKEN}@}" + git -C "${tmp_dir}" push --force "${token_remote_url}" previews:quality-report-previews >/dev/null + + commit_sha=$(git -C "${tmp_dir}" rev-parse HEAD) + raw_base="https://raw.githubusercontent.com/${REPOSITORY}/${commit_sha}/${run_dir}" + preview_base="https://htmlpreview.github.io/?${raw_base}" + echo "jacoco_url=${preview_base}/coverage/index.html" >> "$GITHUB_OUTPUT" + + - name: Generate ByteCodeTranslator quality report + if: ${{ always() }} + env: + QUALITY_REPORT_TARGET_DIRS: vm/tests/target + QUALITY_REPORT_SERVER_URL: ${{ github.server_url }} + QUALITY_REPORT_REPOSITORY: ${{ github.repository }} + QUALITY_REPORT_REF: ${{ github.event.pull_request.head.sha || github.sha }} + QUALITY_REPORT_TITLE: "✅ ByteCodeTranslator Quality Report" + JACOCO_HTML_URL: ${{ steps.publish-bytecode-quality-previews.outputs.jacoco_url }} + run: python3 .github/scripts/generate-quality-report.py + + - name: Publish ByteCodeTranslator quality comment + if: ${{ github.event_name == 'pull_request' }} + uses: actions/github-script@v7 + with: + script: | + const { publishQualityComment } = require('./.github/scripts/publish-quality-comment.js'); + await publishQualityComment({ + github, + context, + core, + marker: '', + reportPath: 'quality-report.md', + }); diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a46ff9b860..b3604dc0f8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -205,40 +205,8 @@ jobs: uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - const marker = ''; - const reportPath = 'quality-report.md'; - if (!fs.existsSync(reportPath)) { - core.warning('quality-report.md was not generated.'); - return; - } - const body = `${marker}\n${fs.readFileSync(reportPath, 'utf8')}`; - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number, - per_page: 100, - }); - const existing = comments.find( - (comment) => comment.user?.type === 'Bot' && comment.body?.includes(marker), - ); - if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body, - }); - } + const { publishQualityComment } = require('./.github/scripts/publish-quality-comment.js'); + await publishQualityComment({ github, context, core }); - name: Install dependencies run: | sudo apt-get update && sudo apt-get install xvfb diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 744463e2aa..425f6c7a90 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -57,7 +57,14 @@ public String extension() { public String extension() { return "cs"; } - + + }, + OUTPUT_TYPE_CLEAN { + @Override + public String extension() { + return "c"; + } + }; public abstract String extension(); @@ -164,7 +171,7 @@ public static void main(String[] args) throws Exception { } if(args.length != 9) { - System.out.println("We accept 9 arguments output type (ios, csharp), input directory, output directory, app name, package name, app dispaly name, version, type (ios/iphone/ipad) and additional frameworks"); + System.out.println("We accept 9 arguments output type (ios, csharp, clean), input directory, output directory, app name, package name, app dispaly name, version, type (ios/iphone/ipad) and additional frameworks"); System.exit(1); return; } @@ -181,6 +188,8 @@ public static void main(String[] args) throws Exception { } if(args[0].equalsIgnoreCase("csharp")) { output = OutputType.OUTPUT_TYPE_CSHARP; + } else if(args[0].equalsIgnoreCase("clean")) { + output = OutputType.OUTPUT_TYPE_CLEAN; } String[] sourceDirectories = args[1].split(";"); File[] sources = new File[sourceDirectories.length]; @@ -200,305 +209,359 @@ public static void main(String[] args) throws Exception { } ByteCodeTranslator b = new ByteCodeTranslator(); - if(output == OutputType.OUTPUT_TYPE_IOS) { - File root = new File(dest, "dist"); - root.mkdirs(); - System.out.println("Root is: " + root.getAbsolutePath()); - File srcRoot = new File(root, appName + "-src"); - srcRoot.mkdirs(); - //cleanDir(srcRoot); - - System.out.println("srcRoot is: " + srcRoot.getAbsolutePath() ); - - File imagesXcassets = new File(srcRoot, "Images.xcassets"); - imagesXcassets.mkdirs(); - //cleanDir(imagesXcassets); + switch (output) { + case OUTPUT_TYPE_IOS: + handleIosOutput(b, sources, dest, appName, appPackageName, appDisplayName, appVersion, appType, addFrameworks); + break; + case OUTPUT_TYPE_CLEAN: + handleCleanOutput(b, sources, dest, appName); + break; + default: + handleDefaultOutput(b, sources, dest); + } + } - File launchImageLaunchimage = new File(imagesXcassets, "LaunchImage.launchimage"); - launchImageLaunchimage.mkdirs(); - //cleanDir(launchImageLaunchimage); - - copy(ByteCodeTranslator.class.getResourceAsStream("/LaunchImages.json"), new FileOutputStream(new File(launchImageLaunchimage, "Contents.json"))); + private static void handleDefaultOutput(ByteCodeTranslator b, File[] sources, File dest) throws Exception { + b.execute(sources, dest); + Parser.writeOutput(dest); + } - File appIconAppiconset = new File(imagesXcassets, "AppIcon.appiconset"); - appIconAppiconset.mkdirs(); - //cleanDir(appIconAppiconset); + private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File dest, String appName) throws Exception { + File root = new File(dest, "dist"); + root.mkdirs(); + System.out.println("Root is: " + root.getAbsolutePath()); + File srcRoot = new File(root, appName + "-src"); + srcRoot.mkdirs(); - copy(ByteCodeTranslator.class.getResourceAsStream("/Icons.json"), new FileOutputStream(new File(appIconAppiconset, "Contents.json"))); - - - File xcproj = new File(root, appName + ".xcodeproj"); - xcproj.mkdirs(); - //cleanDir(xcproj); - - File projectXCworkspace = new File(xcproj, "project.xcworkspace"); - projectXCworkspace.mkdirs(); - //cleanDir(projectXCworkspace); - - /*File xcsharedData = new File(projectXCworkspace, "xcshareddata"); - xcsharedData.mkdirs();*/ - - b.execute(sources, srcRoot); + b.execute(sources, srcRoot); - File cn1Globals = new File(srcRoot, "cn1_globals.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.h"), new FileOutputStream(cn1Globals)); - if (System.getProperty("INCLUDE_NPE_CHECKS", "false").equals("true")) { - replaceInFile(cn1Globals, "//#define CN1_INCLUDE_NPE_CHECKS", "#define CN1_INCLUDE_NPE_CHECKS"); - } - File cn1GlobalsM = new File(srcRoot, "cn1_globals.m"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.m"), new FileOutputStream(cn1GlobalsM)); - File nativeMethods = new File(srcRoot, "nativeMethods.m"); - copy(ByteCodeTranslator.class.getResourceAsStream("/nativeMethods.m"), new FileOutputStream(nativeMethods)); - - if (System.getProperty("USE_RPMALLOC", "false").equals("true")) { - File malloc = new File(srcRoot, "malloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/malloc.c"), new FileOutputStream(malloc)); - File rpmalloc = new File(srcRoot, "rpmalloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.c"), new FileOutputStream(rpmalloc)); - File rpmalloch = new File(srcRoot, "rpmalloc.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.h"), new FileOutputStream(rpmalloch)); - } - - Parser.writeOutput(srcRoot); - - File templateInfoPlist = new File(srcRoot, appName + "-Info.plist"); - copy(ByteCodeTranslator.class.getResourceAsStream("/template/template/template-Info.plist"), new FileOutputStream(templateInfoPlist)); - - File templatePch = new File(srcRoot, appName + "-Prefix.pch"); - copy(ByteCodeTranslator.class.getResourceAsStream("/template/template/template-Prefix.pch"), new FileOutputStream(templatePch)); + File cn1Globals = new File(srcRoot, "cn1_globals.h"); + copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.h"), new FileOutputStream(cn1Globals)); + if (System.getProperty("INCLUDE_NPE_CHECKS", "false").equals("true")) { + replaceInFile(cn1Globals, "//#define CN1_INCLUDE_NPE_CHECKS", "#define CN1_INCLUDE_NPE_CHECKS"); + } + File xmlvm = new File(srcRoot, "xmlvm.h"); + copy(ByteCodeTranslator.class.getResourceAsStream("/xmlvm.h"), new FileOutputStream(xmlvm)); + File nativeMethods = new File(srcRoot, "nativeMethods.m"); + copy(ByteCodeTranslator.class.getResourceAsStream("/nativeMethods.m"), new FileOutputStream(nativeMethods)); - File xmlvm = new File(srcRoot, "xmlvm.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/xmlvm.h"), new FileOutputStream(xmlvm)); - - File projectWorkspaceData = new File(projectXCworkspace, "contents.xcworkspacedata"); - copy(ByteCodeTranslator.class.getResourceAsStream("/template/template.xcodeproj/project.xcworkspace/contents.xcworkspacedata"), new FileOutputStream(projectWorkspaceData)); - replaceInFile(projectWorkspaceData, "KitchenSink", appName); - - - File projectPbx = new File(xcproj, "project.pbxproj"); - copy(ByteCodeTranslator.class.getResourceAsStream("/template/template.xcodeproj/project.pbxproj"), new FileOutputStream(projectPbx)); - - String[] sourceFiles = srcRoot.list(new FilenameFilter() { - @Override - public boolean accept(File pathname, String string) { - return string.endsWith(".bundle") || string.endsWith(".xcdatamodeld") || !pathname.isHidden() && !string.startsWith(".") && !"Images.xcassets".equals(string); - } - }); - - StringBuilder fileOneEntry = new StringBuilder(); - StringBuilder fileTwoEntry = new StringBuilder(); - StringBuilder fileListEntry = new StringBuilder(); - StringBuilder fileThreeEntry = new StringBuilder(); - StringBuilder frameworks = new StringBuilder(); - StringBuilder frameworks2 = new StringBuilder(); - StringBuilder resources = new StringBuilder(); - - List noArcFiles = new ArrayList(); - noArcFiles.add("CVZBarReaderViewController.m"); - noArcFiles.add("OpenUDID.m"); - - List includeFrameworks = new ArrayList(); - Set optionalFrameworks = new HashSet(); - for (String optionalFramework : System.getProperty("optional.frameworks", "").split(";")) { - optionalFramework = optionalFramework.trim(); - if (!optionalFramework.isEmpty()) { - optionalFrameworks.add(optionalFramework); - } + Parser.writeOutput(srcRoot); + + writeCmakeProject(root, srcRoot, appName); + } + + private static void handleIosOutput(ByteCodeTranslator b, File[] sources, File dest, String appName, String appPackageName, String appDisplayName, String appVersion, String appType, String addFrameworks) throws Exception { + File root = new File(dest, "dist"); + root.mkdirs(); + System.out.println("Root is: " + root.getAbsolutePath()); + File srcRoot = new File(root, appName + "-src"); + srcRoot.mkdirs(); + //cleanDir(srcRoot); + + System.out.println("srcRoot is: " + srcRoot.getAbsolutePath() ); + + File imagesXcassets = new File(srcRoot, "Images.xcassets"); + imagesXcassets.mkdirs(); + //cleanDir(imagesXcassets); + + File launchImageLaunchimage = new File(imagesXcassets, "LaunchImage.launchimage"); + launchImageLaunchimage.mkdirs(); + //cleanDir(launchImageLaunchimage); + + copy(ByteCodeTranslator.class.getResourceAsStream("/LaunchImages.json"), new FileOutputStream(new File(launchImageLaunchimage, "Contents.json"))); + + File appIconAppiconset = new File(imagesXcassets, "AppIcon.appiconset"); + appIconAppiconset.mkdirs(); + //cleanDir(appIconAppiconset); + + copy(ByteCodeTranslator.class.getResourceAsStream("/Icons.json"), new FileOutputStream(new File(appIconAppiconset, "Contents.json"))); + + + File xcproj = new File(root, appName + ".xcodeproj"); + xcproj.mkdirs(); + //cleanDir(xcproj); + + File projectXCworkspace = new File(xcproj, "project.xcworkspace"); + projectXCworkspace.mkdirs(); + //cleanDir(projectXCworkspace); + + /*File xcsharedData = new File(projectXCworkspace, "xcshareddata"); + xcsharedData.mkdirs();*/ + + b.execute(sources, srcRoot); + + File cn1Globals = new File(srcRoot, "cn1_globals.h"); + copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.h"), new FileOutputStream(cn1Globals)); + if (System.getProperty("INCLUDE_NPE_CHECKS", "false").equals("true")) { + replaceInFile(cn1Globals, "//#define CN1_INCLUDE_NPE_CHECKS", "#define CN1_INCLUDE_NPE_CHECKS"); + } + File cn1GlobalsM = new File(srcRoot, "cn1_globals.m"); + copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.m"), new FileOutputStream(cn1GlobalsM)); + File nativeMethods = new File(srcRoot, "nativeMethods.m"); + copy(ByteCodeTranslator.class.getResourceAsStream("/nativeMethods.m"), new FileOutputStream(nativeMethods)); + + if (System.getProperty("USE_RPMALLOC", "false").equals("true")) { + File malloc = new File(srcRoot, "malloc.c"); + copy(ByteCodeTranslator.class.getResourceAsStream("/malloc.c"), new FileOutputStream(malloc)); + File rpmalloc = new File(srcRoot, "rpmalloc.c"); + copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.c"), new FileOutputStream(rpmalloc)); + File rpmalloch = new File(srcRoot, "rpmalloc.h"); + copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.h"), new FileOutputStream(rpmalloch)); + } + + Parser.writeOutput(srcRoot); + + File templateInfoPlist = new File(srcRoot, appName + "-Info.plist"); + copy(ByteCodeTranslator.class.getResourceAsStream("/template/template/template-Info.plist"), new FileOutputStream(templateInfoPlist)); + + File templatePch = new File(srcRoot, appName + "-Prefix.pch"); + copy(ByteCodeTranslator.class.getResourceAsStream("/template/template/template-Prefix.pch"), new FileOutputStream(templatePch)); + + File xmlvm = new File(srcRoot, "xmlvm.h"); + copy(ByteCodeTranslator.class.getResourceAsStream("/xmlvm.h"), new FileOutputStream(xmlvm)); + + File projectWorkspaceData = new File(projectXCworkspace, "contents.xcworkspacedata"); + copy(ByteCodeTranslator.class.getResourceAsStream("/template/template.xcodeproj/project.xcworkspace/contents.xcworkspacedata"), new FileOutputStream(projectWorkspaceData)); + replaceInFile(projectWorkspaceData, "KitchenSink", appName); + + + File projectPbx = new File(xcproj, "project.pbxproj"); + copy(ByteCodeTranslator.class.getResourceAsStream("/template/template.xcodeproj/project.pbxproj"), new FileOutputStream(projectPbx)); + + String[] sourceFiles = srcRoot.list(new FilenameFilter() { + @Override + public boolean accept(File pathname, String string) { + return string.endsWith(".bundle") || string.endsWith(".xcdatamodeld") || !pathname.isHidden() && !string.startsWith(".") && !"Images.xcassets".equals(string); } - optionalFrameworks.add("UserNotifications.framework"); - includeFrameworks.add("libiconv.dylib"); - //includeFrameworks.add("AdSupport.framework"); - includeFrameworks.add("AddressBookUI.framework"); - includeFrameworks.add("SystemConfiguration.framework"); - includeFrameworks.add("MapKit.framework"); - includeFrameworks.add("AudioToolbox.framework"); - includeFrameworks.add("libxml2.dylib"); - includeFrameworks.add("QuartzCore.framework"); - includeFrameworks.add("AddressBook.framework"); - includeFrameworks.add("libsqlite3.dylib"); - includeFrameworks.add("libsqlite3.0.dylib"); - includeFrameworks.add("GameKit.framework"); - includeFrameworks.add("Security.framework"); - //includeFrameworks.add("StoreKit.framework"); - includeFrameworks.add("CoreMotion.framework"); - includeFrameworks.add("CoreLocation.framework"); - includeFrameworks.add("MessageUI.framework"); - includeFrameworks.add("MediaPlayer.framework"); - includeFrameworks.add("AVFoundation.framework"); - includeFrameworks.add("CoreVideo.framework"); - includeFrameworks.add("QuickLook.framework"); - //includeFrameworks.add("iAd.framework"); - includeFrameworks.add("CoreMedia.framework"); - includeFrameworks.add("libz.dylib"); - includeFrameworks.add("MobileCoreServices.framework"); - includeFrameworks.add("AVKit.framework"); - if(!addFrameworks.equalsIgnoreCase("none")) { - includeFrameworks.addAll(Arrays.asList(addFrameworks.split(";"))); + }); + + StringBuilder fileOneEntry = new StringBuilder(); + StringBuilder fileTwoEntry = new StringBuilder(); + StringBuilder fileListEntry = new StringBuilder(); + StringBuilder fileThreeEntry = new StringBuilder(); + StringBuilder frameworks = new StringBuilder(); + StringBuilder frameworks2 = new StringBuilder(); + StringBuilder resources = new StringBuilder(); + + List noArcFiles = new ArrayList(); + noArcFiles.add("CVZBarReaderViewController.m"); + noArcFiles.add("OpenUDID.m"); + + List includeFrameworks = new ArrayList(); + Set optionalFrameworks = new HashSet(); + for (String optionalFramework : System.getProperty("optional.frameworks", "").split(";")) { + optionalFramework = optionalFramework.trim(); + if (!optionalFramework.isEmpty()) { + optionalFrameworks.add(optionalFramework); } - - int currentValue = 0xF63EAAA; + } + optionalFrameworks.add("UserNotifications.framework"); + includeFrameworks.add("libiconv.dylib"); + //includeFrameworks.add("AdSupport.framework"); + includeFrameworks.add("AddressBookUI.framework"); + includeFrameworks.add("SystemConfiguration.framework"); + includeFrameworks.add("MapKit.framework"); + includeFrameworks.add("AudioToolbox.framework"); + includeFrameworks.add("libxml2.dylib"); + includeFrameworks.add("QuartzCore.framework"); + includeFrameworks.add("AddressBook.framework"); + includeFrameworks.add("libsqlite3.dylib"); + includeFrameworks.add("libsqlite3.0.dylib"); + includeFrameworks.add("GameKit.framework"); + includeFrameworks.add("Security.framework"); + //includeFrameworks.add("StoreKit.framework"); + includeFrameworks.add("CoreMotion.framework"); + includeFrameworks.add("CoreLocation.framework"); + includeFrameworks.add("MessageUI.framework"); + includeFrameworks.add("MediaPlayer.framework"); + includeFrameworks.add("AVFoundation.framework"); + includeFrameworks.add("CoreVideo.framework"); + includeFrameworks.add("QuickLook.framework"); + //includeFrameworks.add("iAd.framework"); + includeFrameworks.add("CoreMedia.framework"); + includeFrameworks.add("libz.dylib"); + includeFrameworks.add("MobileCoreServices.framework"); + includeFrameworks.add("AVKit.framework"); + if(!addFrameworks.equalsIgnoreCase("none")) { + includeFrameworks.addAll(Arrays.asList(addFrameworks.split(";"))); + } - ArrayList arr = new ArrayList(); - arr.addAll(includeFrameworks); - arr.addAll(Arrays.asList(sourceFiles)); - - for(String file : arr) { - fileListEntry.append(" 0"); - currentValue++; - String fileOneValue = Integer.toHexString(currentValue).toUpperCase(); - fileListEntry.append(fileOneValue); - fileListEntry.append("18E9ABBC002F3D1D /* "); + int currentValue = 0xF63EAAA; + + ArrayList arr = new ArrayList(); + arr.addAll(includeFrameworks); + arr.addAll(Arrays.asList(sourceFiles)); + + for(String file : arr) { + fileListEntry.append(" 0"); + currentValue++; + String fileOneValue = Integer.toHexString(currentValue).toUpperCase(); + fileListEntry.append(fileOneValue); + fileListEntry.append("18E9ABBC002F3D1D /* "); + fileListEntry.append(file); + fileListEntry.append(" */ = {isa = PBXFileReference; lastKnownFileType = "); + fileListEntry.append(getFileType(file)); + if(file.endsWith(".framework") || file.endsWith(".dylib") || file.endsWith(".a")) { + fileListEntry.append("; name = \""); fileListEntry.append(file); - fileListEntry.append(" */ = {isa = PBXFileReference; lastKnownFileType = "); - fileListEntry.append(getFileType(file)); - if(file.endsWith(".framework") || file.endsWith(".dylib") || file.endsWith(".a")) { - fileListEntry.append("; name = \""); + if(file.endsWith(".dylib")) { + fileListEntry.append("\"; path = \"usr/lib/"); fileListEntry.append(file); - if(file.endsWith(".dylib")) { - fileListEntry.append("\"; path = \"usr/lib/"); - fileListEntry.append(file); - fileListEntry.append("\"; sourceTree = SDKROOT; };\n"); - } else { - if(file.endsWith(".a")) { - fileListEntry.append("\"; path = \""); - fileListEntry.append(appName); - fileListEntry.append("-src/"); - fileListEntry.append(file); - fileListEntry.append("\"; sourceTree = \"\"; };\n"); - } else { - fileListEntry.append("\"; path = System/Library/Frameworks/"); - fileListEntry.append(file); - fileListEntry.append("; sourceTree = SDKROOT; };\n"); - } - } + fileListEntry.append("\"; sourceTree = SDKROOT; };\n"); } else { - fileListEntry.append("; path = \""); - if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".mm") || file.endsWith(".h") || - file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".hh") || file.endsWith(".hpp") || file.endsWith(".xib")) { - fileListEntry.append(file); - } else { + if(file.endsWith(".a")) { + fileListEntry.append("\"; path = \""); fileListEntry.append(appName); fileListEntry.append("-src/"); fileListEntry.append(file); + fileListEntry.append("\"; sourceTree = \"\"; };\n"); + } else { + fileListEntry.append("\"; path = System/Library/Frameworks/"); + fileListEntry.append(file); + fileListEntry.append("; sourceTree = SDKROOT; };\n"); } - fileListEntry.append("\"; sourceTree = \"\"; };\n"); - } - currentValue++; - fileOneEntry.append(" 0"); - String referenceValue = Integer.toHexString(currentValue).toUpperCase(); - fileOneEntry.append(referenceValue); - fileOneEntry.append("18E9ABBC002F3D1D /* "); - fileOneEntry.append(file); - fileOneEntry.append(" */ = {isa = PBXBuildFile; fileRef = 0"); - fileOneEntry.append(fileOneValue); - fileOneEntry.append("18E9ABBC002F3D1D /* "); - fileOneEntry.append(file); - String injectFileSettings = ""; - if (optionalFrameworks.contains(file)) { - injectFileSettings += " ATTRIBUTES = (Weak, );"; } - String fileSettingsDefault = "settings = {"+injectFileSettings.trim()+" }; "; - if(noArcFiles.contains(file)) { - fileOneEntry.append(" */; settings = {COMPILER_FLAGS = \"-fno-objc-arc\";"+injectFileSettings+" }; };\n"); + } else { + fileListEntry.append("; path = \""); + if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".mm") || file.endsWith(".h") || + file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".hh") || file.endsWith(".hpp") || file.endsWith(".xib")) { + fileListEntry.append(file); } else { - fileOneEntry.append(" */;"+fileSettingsDefault+" };\n"); + fileListEntry.append(appName); + fileListEntry.append("-src/"); + fileListEntry.append(file); } + fileListEntry.append("\"; sourceTree = \"\"; };\n"); + } + currentValue++; + fileOneEntry.append(" 0"); + String referenceValue = Integer.toHexString(currentValue).toUpperCase(); + fileOneEntry.append(referenceValue); + fileOneEntry.append("18E9ABBC002F3D1D /* "); + fileOneEntry.append(file); + fileOneEntry.append(" */ = {isa = PBXBuildFile; fileRef = 0"); + fileOneEntry.append(fileOneValue); + fileOneEntry.append("18E9ABBC002F3D1D /* "); + fileOneEntry.append(file); + String injectFileSettings = ""; + if (optionalFrameworks.contains(file)) { + injectFileSettings += " ATTRIBUTES = (Weak, );"; + } + String fileSettingsDefault = "settings = {"+injectFileSettings.trim()+" }; "; + if(noArcFiles.contains(file)) { + fileOneEntry.append(" */; settings = {COMPILER_FLAGS = \"-fno-objc-arc\";"+injectFileSettings+" }; };\n"); + } else { + fileOneEntry.append(" */;"+fileSettingsDefault+" };\n"); + } + + if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".hh") || file.endsWith(".hpp") || + file.endsWith(".mm") || file.endsWith(".h") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".xib")) { - if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".hh") || file.endsWith(".hpp") || - file.endsWith(".mm") || file.endsWith(".h") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".xib")) { + // bundle also needs to be a runtime resource + if(file.endsWith(".bundle") || file.endsWith(".xcdatamodeld")) { + resources.append("\n 0"); + resources.append(referenceValue); + resources.append("18E9ABBC002F3D1D /* "); + resources.append(file); + resources.append(" */,"); + } + + fileTwoEntry.append(" 0"); + fileTwoEntry.append(fileOneValue); + fileTwoEntry.append("18E9ABBC002F3D1D /* "); + fileTwoEntry.append(file); + fileTwoEntry.append(" */,\n"); + + if(!file.endsWith(".h") && !file.endsWith(".hpp") && !file.endsWith(".hh") && !file.endsWith(".bundle")) { + fileThreeEntry.append(" 0"); + fileThreeEntry.append(referenceValue); + fileThreeEntry.append("18E9ABBC002F3D1D /* "); + fileThreeEntry.append(file); + fileThreeEntry.append(" */,\n"); + } + } else { + if(file.endsWith(".a") || file.endsWith(".framework") || file.endsWith(".dylib") || (file.endsWith("Info.plist") && !"GoogleService-Info.plist".equals(file)) || file.endsWith(".pch")) { + frameworks.append(" 0"); + frameworks.append(referenceValue); + frameworks.append("18E9ABBC002F3D1D /* "); + frameworks.append(file); + frameworks.append(" */,\n"); + + frameworks2.append(" 0"); + frameworks2.append(fileOneValue); + frameworks2.append("18E9ABBC002F3D1D /* "); + frameworks2.append(file); + frameworks2.append(" */,\n"); - // bundle also needs to be a runtime resource - if(file.endsWith(".bundle") || file.endsWith(".xcdatamodeld")) { - resources.append("\n 0"); - resources.append(referenceValue); - resources.append("18E9ABBC002F3D1D /* "); - resources.append(file); - resources.append(" */,"); - } - fileTwoEntry.append(" 0"); - fileTwoEntry.append(fileOneValue); - fileTwoEntry.append("18E9ABBC002F3D1D /* "); - fileTwoEntry.append(file); - fileTwoEntry.append(" */,\n"); - - if(!file.endsWith(".h") && !file.endsWith(".hpp") && !file.endsWith(".hh") && !file.endsWith(".bundle")) { - fileThreeEntry.append(" 0"); - fileThreeEntry.append(referenceValue); - fileThreeEntry.append("18E9ABBC002F3D1D /* "); - fileThreeEntry.append(file); - fileThreeEntry.append(" */,\n"); - } - } else { - if(file.endsWith(".a") || file.endsWith(".framework") || file.endsWith(".dylib") || (file.endsWith("Info.plist") && !"GoogleService-Info.plist".equals(file)) || file.endsWith(".pch")) { - frameworks.append(" 0"); - frameworks.append(referenceValue); - frameworks.append("18E9ABBC002F3D1D /* "); - frameworks.append(file); - frameworks.append(" */,\n"); - - frameworks2.append(" 0"); - frameworks2.append(fileOneValue); - frameworks2.append("18E9ABBC002F3D1D /* "); - frameworks2.append(file); - frameworks2.append(" */,\n"); - - - - /* - // Removing this because it causes crashes in cocoapods. - // Why was it necessary to add .a files to the same group - // as the sources, if we've already added it to frameworks. - // Related to https://stackoverflow.com/questions/47210585/codename-one-issue-devilering-binary-for-ios - if(file.endsWith(".a")) { - fileTwoEntry.append(" 0"); - fileTwoEntry.append(fileOneValue); - fileTwoEntry.append("18E9ABBC002F3D1D /* "); - fileTwoEntry.append(file); - fileTwoEntry.append(" *").append("/,\n"); - - if(!file.endsWith(".h") && !file.endsWith(".bundle") && !file.endsWith(".xcdatamodeld")) { - fileThreeEntry.append(" 0"); - fileThreeEntry.append(referenceValue); - fileThreeEntry.append("18E9ABBC002F3D1D /* "); - fileThreeEntry.append(file); - fileThreeEntry.append(" *").append("/,\n"); - } - }*/ - } else { - // standard resource file - resources.append("\n 0"); - resources.append(referenceValue); - resources.append("18E9ABBC002F3D1D /* "); - resources.append(file); - resources.append(" */,"); - } + /* + + // Removing this because it causes crashes in cocoapods. + // Why was it necessary to add .a files to the same group + // as the sources, if we've already added it to frameworks. + // Related to https://stackoverflow.com/questions/47210585/codename-one-issue-devilering-binary-for-ios + if(file.endsWith(".a")) { + fileTwoEntry.append(" 0"); + fileTwoEntry.append(fileOneValue); + fileTwoEntry.append("18E9ABBC002F3D1D /* "); + fileTwoEntry.append(file); + fileTwoEntry.append(" *").append("/,\n"); + + if(!file.endsWith(".h") && !file.endsWith(".bundle") && !file.endsWith(".xcdatamodeld")) { + fileThreeEntry.append(" 0"); + fileThreeEntry.append(referenceValue); + fileThreeEntry.append("18E9ABBC002F3D1D /* "); + fileThreeEntry.append(file); + fileThreeEntry.append(" *").append("/,\n"); + } + }*/ + } else { + // standard resource file + resources.append("\n 0"); + resources.append(referenceValue); + resources.append("18E9ABBC002F3D1D /* "); + resources.append(file); + resources.append(" */,"); } } - - if(!appType.equalsIgnoreCase("ios")) { - String devFamily = "TARGETED_DEVICE_FAMILY = \"2\";"; - if(appType.equalsIgnoreCase("iphone")) { - devFamily = "TARGETED_DEVICE_FAMILY = \"1\";"; - } - replaceInFile(projectPbx, "template", appName, "**ACTUAL_FILES**", fileListEntry.toString(), - "**FILE_LIST**", fileOneEntry.toString(), "** FILE_LIST_2 **", fileTwoEntry.toString(), - "**FILES_3**", fileThreeEntry.toString(), "***FRAMEWORKS***", frameworks.toString(), - "***FRAMEWORKS2***", frameworks2.toString(), "TARGETED_DEVICE_FAMILY = \"1,2\";", devFamily, - "***RESOURCES***", resources.toString()); - } else { - replaceInFile(projectPbx, "template", appName, "**ACTUAL_FILES**", fileListEntry.toString(), - "**FILE_LIST**", fileOneEntry.toString(), "** FILE_LIST_2 **", fileTwoEntry.toString(), - "**FILES_3**", fileThreeEntry.toString(), "***FRAMEWORKS***", frameworks.toString(), - "***FRAMEWORKS2***", frameworks2.toString(), "***RESOURCES***", resources.toString()); - } + } - String bundleVersion = System.getProperty("bundleVersionNumber", appVersion); - replaceInFile(templateInfoPlist, "com.codename1pkg", appPackageName, "${PRODUCT_NAME}", appDisplayName, "VERSION_VALUE", appVersion, "VERSION_BUNDLE_VALUE", bundleVersion); + if(!appType.equalsIgnoreCase("ios")) { + String devFamily = "TARGETED_DEVICE_FAMILY = \"2\";"; + if(appType.equalsIgnoreCase("iphone")) { + devFamily = "TARGETED_DEVICE_FAMILY = \"1\";"; + } + replaceInFile(projectPbx, "template", appName, "**ACTUAL_FILES**", fileListEntry.toString(), + "**FILE_LIST**", fileOneEntry.toString(), "** FILE_LIST_2 **", fileTwoEntry.toString(), + "**FILES_3**", fileThreeEntry.toString(), "***FRAMEWORKS***", frameworks.toString(), + "***FRAMEWORKS2***", frameworks2.toString(), "TARGETED_DEVICE_FAMILY = \"1,2\";", devFamily, + "***RESOURCES***", resources.toString()); } else { - b.execute(sources, dest); - Parser.writeOutput(dest); + replaceInFile(projectPbx, "template", appName, "**ACTUAL_FILES**", fileListEntry.toString(), + "**FILE_LIST**", fileOneEntry.toString(), "** FILE_LIST_2 **", fileTwoEntry.toString(), + "**FILES_3**", fileThreeEntry.toString(), "***FRAMEWORKS***", frameworks.toString(), + "***FRAMEWORKS2***", frameworks2.toString(), "***RESOURCES***", resources.toString()); + } + + String bundleVersion = System.getProperty("bundleVersionNumber", appVersion); + replaceInFile(templateInfoPlist, "com.codename1pkg", appPackageName, "${PRODUCT_NAME}", appDisplayName, "VERSION_VALUE", appVersion, "VERSION_BUNDLE_VALUE", bundleVersion); + } + + private static void writeCmakeProject(File projectRoot, File srcRoot, String appName) throws IOException { + File cmakeLists = new File(projectRoot, "CMakeLists.txt"); + FileWriter writer = new FileWriter(cmakeLists); + try { + writer.append("cmake_minimum_required(VERSION 3.10)\n"); + writer.append("project(").append(appName).append(" LANGUAGES C OBJC)\n"); + writer.append("enable_language(OBJC OPTIONAL)\n"); + writer.append("set(CMAKE_C_STANDARD 99)\n"); + writer.append("file(GLOB TRANSLATOR_SOURCES \"").append(srcRoot.getName()).append("/*.c\" \"").append(srcRoot.getName()).append("/*.m\")\n"); + writer.append("file(GLOB TRANSLATOR_HEADERS \"").append(srcRoot.getName()).append("/*.h\")\n"); + writer.append("add_library(${PROJECT_NAME} ${TRANSLATOR_SOURCES} ${TRANSLATOR_HEADERS})\n"); + writer.append("target_include_directories(${PROJECT_NAME} PUBLIC ").append(srcRoot.getName()).append(")\n"); + } finally { + writer.close(); } } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index 7157f5155a..af51011b02 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -466,7 +466,7 @@ private static void readNativeFiles(File outputDirectory) throws IOException { File[] mFiles = outputDirectory.listFiles(new FileFilter() { @Override public boolean accept(File file) { - return file.getName().endsWith(".m"); + return file.getName().endsWith(".m") || file.getName().endsWith("." + ByteCodeTranslator.output.extension()); } }); nativeSources = new String[mFiles.length]; @@ -633,18 +633,18 @@ private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFi if (outMain instanceof ConcatenatingFileOutputStream) { ((ConcatenatingFileOutputStream)outMain).beginNextFile(cls.getClsName()); } - if(ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_IOS) { + if(ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_CSHARP) { + outMain.write(cls.generateCSharpCode().getBytes()); + outMain.close(); + } else { outMain.write(cls.generateCCode(classes).getBytes()); outMain.close(); - // we also need to write the header file for iOS + // we also need to write the header file for C outputs String headerName = cls.getClsName() + ".h"; FileOutputStream outHeader = new FileOutputStream(new File(outputDir, headerName)); outHeader.write(cls.generateCHeader().getBytes()); outHeader.close(); - } else { - outMain.write(cls.generateCSharpCode().getBytes()); - outMain.close(); } } diff --git a/vm/tests/pom.xml b/vm/tests/pom.xml index 913793a582..9abcbd7b4f 100644 --- a/vm/tests/pom.xml +++ b/vm/tests/pom.xml @@ -64,6 +64,25 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + test + + report + + + + diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java new file mode 100644 index 0000000000..0818698c69 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -0,0 +1,304 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class CleanTargetIntegrationTest { + + @Test + void generatesRunnableHelloWorldUsingCleanTarget() throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("clean-target-sources"); + Path classesDir = Files.createTempDirectory("clean-target-classes"); + Path javaFile = sourceDir.resolve("HelloWorld.java"); + Files.createDirectories(sourceDir.resolve("java/lang")); + Files.write(javaFile, helloWorldSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Object.java"), javaLangObjectSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/String.java"), javaLangStringSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Class.java"), javaLangClassSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Throwable.java"), javaLangThrowableSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Exception.java"), javaLangExceptionSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/RuntimeException.java"), javaLangRuntimeExceptionSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/NullPointerException.java"), javaLangNullPointerExceptionSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("native_hello.c"), nativeHelloSource().getBytes(StandardCharsets.UTF_8)); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "A JDK is required to compile test sources"); + int compileResult = compiler.run( + null, + null, + null, + "-d", classesDir.toString(), + javaFile.toString(), + sourceDir.resolve("java/lang/Object.java").toString(), + sourceDir.resolve("java/lang/String.java").toString(), + sourceDir.resolve("java/lang/Class.java").toString(), + sourceDir.resolve("java/lang/Throwable.java").toString(), + sourceDir.resolve("java/lang/Exception.java").toString(), + sourceDir.resolve("java/lang/RuntimeException.java").toString(), + sourceDir.resolve("java/lang/NullPointerException.java").toString() + ); + assertEquals(0, compileResult, "HelloWorld.java should compile"); + + Files.copy(sourceDir.resolve("native_hello.c"), classesDir.resolve("native_hello.c")); + + Path outputDir = Files.createTempDirectory("clean-target-output"); + runTranslator(classesDir, outputDir, "HelloCleanApp"); + + Path distDir = outputDir.resolve("dist"); + Path cmakeLists = distDir.resolve("CMakeLists.txt"); + assertTrue(Files.exists(cmakeLists), "Translator should emit a CMake project"); + + Path srcRoot = distDir.resolve("HelloCleanApp-src"); + patchCn1Globals(srcRoot); + writeRuntimeStubs(srcRoot); + + replaceLibraryWithExecutableTarget(cmakeLists, srcRoot.getFileName().toString()); + + Path buildDir = distDir.resolve("build"); + Files.createDirectories(buildDir); + + runCommand(Arrays.asList( + "cmake", + "-S", distDir.toString(), + "-B", buildDir.toString(), + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang" + ), distDir); + + runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + + Path executable = buildDir.resolve("HelloCleanApp"); + String output = runCommand(Arrays.asList(executable.toString()), buildDir); + + assertTrue(output.contains("Hello, Clean Target!"), + "Compiled program should print hello message, actual output was:\n" + output); + } + + private void runTranslator(Path classesDir, Path outputDir, String appName) throws Exception { + Path translatorResources = Paths.get("..", "ByteCodeTranslator", "src").normalize().toAbsolutePath(); + URLClassLoader systemLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + URL[] systemUrls = systemLoader.getURLs(); + URL[] urls = Arrays.copyOf(systemUrls, systemUrls.length + 1); + urls[systemUrls.length] = translatorResources.toUri().toURL(); + URLClassLoader loader = new URLClassLoader(urls, null); + + ClassLoader originalLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + assertNotNull(loader.getResource("cn1_globals.h"), "Translator resources should be on the classpath"); + Class translatorClass = Class.forName("com.codename1.tools.translator.ByteCodeTranslator", true, loader); + assertEquals(loader, translatorClass.getClassLoader()); + Method main = translatorClass.getMethod("main", String[].class); + String[] args = new String[]{ + "clean", + classesDir.toString(), + outputDir.toString(), + appName, + "com.example.hello", + "Hello App", + "1.0", + "ios", + "none" + }; + try { + main.invoke(null, (Object) args); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause() != null ? ite.getCause() : ite; + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } + } finally { + Thread.currentThread().setContextClassLoader(originalLoader); + try { + loader.close(); + } catch (IOException ignore) { + } + } + } + + private void replaceLibraryWithExecutableTarget(Path cmakeLists, String sourceDirName) throws IOException { + String content = new String(Files.readAllBytes(cmakeLists), StandardCharsets.UTF_8); + String globWithObjc = String.format("file(GLOB TRANSLATOR_SOURCES \"%s/*.c\" \"%s/*.m\")", sourceDirName, sourceDirName); + String globCOnly = String.format("file(GLOB TRANSLATOR_SOURCES \"%s/*.c\")", sourceDirName); + content = content.replace(globWithObjc, globCOnly); + String replacement = content.replace( + "add_library(${PROJECT_NAME} ${TRANSLATOR_SOURCES} ${TRANSLATOR_HEADERS})", + "add_executable(${PROJECT_NAME} ${TRANSLATOR_SOURCES} ${TRANSLATOR_HEADERS})" + ); + Files.write(cmakeLists, replacement.getBytes(StandardCharsets.UTF_8)); + } + + private String runCommand(List command, Path workingDir) throws Exception { + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(workingDir.toFile()); + builder.redirectErrorStream(true); + Process process = builder.start(); + String output; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + output = reader.lines().collect(Collectors.joining("\n")); + } + int exit = process.waitFor(); + assertEquals(0, exit, "Command failed: " + String.join(" ", command) + "\nOutput:\n" + output); + return output; + } + + private void patchCn1Globals(Path srcRoot) throws IOException { + Path cn1Globals = srcRoot.resolve("cn1_globals.h"); + String content = new String(Files.readAllBytes(cn1Globals), StandardCharsets.UTF_8); + if (!content.contains("@class NSString;")) { + content = content.replace("#ifdef __OBJC__\n", "#ifdef __OBJC__\n@class NSString;\n"); + Files.write(cn1Globals, content.getBytes(StandardCharsets.UTF_8)); + } + } + + private void writeRuntimeStubs(Path srcRoot) throws IOException { + Path stubs = srcRoot.resolve("runtime_stubs.c"); + if (Files.exists(stubs)) { + return; + } + String content = "#include \"cn1_globals.h\"\n" + + "#include \n" + + "#include \n" + + "\n" + + "static struct ThreadLocalData globalThreadData;\n" + + "static int runtimeInitialized = 0;\n" + + "\n" + + "static void initThreadState() {\n" + + " memset(&globalThreadData, 0, sizeof(globalThreadData));\n" + + " globalThreadData.threadObjectStack = calloc(64, sizeof(struct elementStruct));\n" + + " globalThreadData.pendingHeapAllocations = calloc(64, sizeof(void*));\n" + + " globalThreadData.callStackClass = calloc(64, sizeof(int));\n" + + " globalThreadData.callStackLine = calloc(64, sizeof(int));\n" + + " globalThreadData.callStackMethod = calloc(64, sizeof(int));\n" + + "}\n" + + "\n" + + "struct ThreadLocalData* getThreadLocalData() {\n" + + " if (!runtimeInitialized) {\n" + + " initThreadState();\n" + + " runtimeInitialized = 1;\n" + + " }\n" + + " return &globalThreadData;\n" + + "}\n" + + "\n" + + "JAVA_OBJECT codenameOneGcMalloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent) {\n" + + " JAVA_OBJECT obj = (JAVA_OBJECT)calloc(1, size);\n" + + " if (obj != JAVA_NULL) {\n" + + " obj->__codenameOneParentClsReference = parent;\n" + + " }\n" + + " return obj;\n" + + "}\n" + + "\n" + + "void codenameOneGcFree(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj) {\n" + + " free(obj);\n" + + "}\n" + + "\n" + + "void initConstantPool() {}\n" + + "\n" + + "void initMethodStack(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, int stackSize, int localsStackSize, int classNameId, int methodNameId) {\n" + + " (void)__cn1ThisObject;\n" + + " (void)stackSize;\n" + + " (void)classNameId;\n" + + " (void)methodNameId;\n" + + " threadStateData->threadObjectStackOffset += localsStackSize;\n" + + "}\n" + + "\n" + + "void releaseForReturn(CODENAME_ONE_THREAD_STATE, int cn1LocalsBeginInThread) {\n" + + " threadStateData->threadObjectStackOffset = cn1LocalsBeginInThread;\n" + + "}\n" + + "\n" + + "void releaseForReturnInException(CODENAME_ONE_THREAD_STATE, int cn1LocalsBeginInThread, int methodBlockOffset) {\n" + + " (void)methodBlockOffset;\n" + + " releaseForReturn(threadStateData, cn1LocalsBeginInThread);\n" + + "}\n" + + "\n" + + "void monitorEnter(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj) { (void)obj; }\n" + + "\n" + + "void monitorExit(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj) { (void)obj; }\n" + + "\n" + + "struct clazz class__java_lang_Class = {0};\n" + + "int currentGcMarkValue = 1;\n"; + + Files.write(stubs, content.getBytes(StandardCharsets.UTF_8)); + } + + private String helloWorldSource() { + return "public class HelloWorld {\n" + + " private static native void nativeHello();\n" + + " public static void main(String[] args) {\n" + + " nativeHello();\n" + + " }\n" + + "}\n"; + } + + private String javaLangObjectSource() { + return "package java.lang;\n" + + "public class Object {\n" + + "}\n"; + } + + private String javaLangStringSource() { + return "package java.lang;\n" + + "public class String extends Object {\n" + + "}\n"; + } + + private String javaLangClassSource() { + return "package java.lang;\n" + + "public final class Class extends Object {\n" + + "}\n"; + } + + private String javaLangThrowableSource() { + return "package java.lang;\n" + + "public class Throwable extends Object {\n" + + "}\n"; + } + + private String javaLangExceptionSource() { + return "package java.lang;\n" + + "public class Exception extends Throwable {\n" + + "}\n"; + } + + private String javaLangRuntimeExceptionSource() { + return "package java.lang;\n" + + "public class RuntimeException extends Exception {\n" + + "}\n"; + } + + private String javaLangNullPointerExceptionSource() { + return "package java.lang;\n" + + "public class NullPointerException extends RuntimeException {\n" + + "}\n"; + } + + private String nativeHelloSource() { + return "#include \"cn1_globals.h\"\n" + + "#include \n" + + "void HelloWorld_nativeHello__(CODENAME_ONE_THREAD_STATE) {\n" + + " printf(\"Hello, Clean Target!\\n\");\n" + + "}\n"; + } +}