diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 19a72c7fb6e..931ab2e807c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -546,6 +546,16 @@ jobs: mvn -B -q -f maven/pom.xml -pl windows,linux -am -DskipTests \ -Dcn1.binaries="${CN1_BINARIES}" compile scripts/check-native-signatures.sh --require-all + # The native warning gate itself runs on the macOS/Windows/Linux legs that + # actually compile C; what runs HERE is its parser, against a hand-authored + # fixture, plus a structural check on the checked-in baselines. Both need no + # compiler, and both cover the ways this gate goes wrong silently: a parser + # that stops recognising a diagnostic reports zero warnings, which is + # indistinguishable from a clean build, and a baseline edited into a shape + # that no longer matches masks nothing while looking like it does. + - name: Check native warning parser and baselines + if: ${{ matrix.java-version == 8 }} + run: scripts/check-native-warnings.sh --self-test - name: Run JavaScript Port smoke integration if: ${{ matrix.java-version == 8 }} working-directory: vm diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index a8a0a6c15af..3dfffb91aad 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -249,6 +249,18 @@ jobs: CN1SS_FAIL_ON_MISMATCH: '1' CN1SS_ALLOWED_MISSING: '0' CN1SS_PORT_ID: ios-gl + # Count this build's compiler warnings and attribute each to whoever owns + # the code. This leg is the one that can: it sets no CN1_IOS_DERIVED_DATA, + # so run-ios-ui-tests.sh wipes its derived data and compiles every + # translation unit from cold -- an incremental build reports no warnings + # and is indistinguishable from a clean codebase. Its path filters are + # also a superset of the other iOS legs', so a PR touching only + # CodenameOne/src is still covered. + CN1_WARNING_CENSUS: '1' + CN1_WARNING_LEG: ios-sim-debug + # build-ios-app.sh stages the manifest under the default artifacts dir, + # which is not the per-step one above. + CN1_WARNING_MANIFEST: ${{ github.workspace }}/artifacts/cn1-source-manifest.txt run: | set -euo pipefail mkdir -p "${ARTIFACTS_DIR}" @@ -285,6 +297,8 @@ jobs: artifacts/*-stats.txt artifacts/vm_time.txt artifacts/xcodebuild-list.txt + artifacts/cn1-source-manifest.txt + artifacts/ios-ui-tests/native-warnings.json if-no-files-found: warn retention-days: 14 diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh index dbacf30e353..8607fed8bb9 100755 --- a/scripts/build-ios-app.sh +++ b/scripts/build-ios-app.sh @@ -179,6 +179,21 @@ stage_bytecode_translator_sources() { find "$out_dir" -maxdepth 2 -type f \( -name '*.m' -o -name '*.c' -o -name '*.h' \) \ | sort > "$listing_file" || true + # The translator's record of where each of those files came from. The listing + # above cannot answer that -- generated code, the ParparVM runtime, the port + # natives and vendored third-party sources are all siblings in one flat + # directory -- and without it a warning in the build log has no owner. It sits + # one level up from the sources, in the project root, because anything left in + # the source directory is swept into the Xcode project's resources phase and + # shipped inside the .app. + local manifest_src="$(dirname "$bt_dir")/cn1-source-manifest.txt" + if [ -f "$manifest_src" ]; then + cp "$manifest_src" "$artifacts_dir/cn1-source-manifest.txt" + bia_log "Staged source manifest from $manifest_src" + else + bia_log "No source manifest at $manifest_src; the warning census cannot attribute this build" + fi + ( cd "$artifacts_dir" zip -qry "$(basename "$zip_file")" "$(basename "$out_dir")" diff --git a/scripts/check-native-warnings.py b/scripts/check-native-warnings.py new file mode 100755 index 00000000000..0c7e582ca43 --- /dev/null +++ b/scripts/check-native-warnings.py @@ -0,0 +1,812 @@ +#!/usr/bin/env python3 +"""Classifies the compiler warnings in a native build log by who owns the code. + +A ParparVM build compiles four completely different kinds of C into one binary: +the code the translator generated, the ParparVM runtime, the hand-written port +natives, and vendored third-party sources. Their warnings all land in one log, +undifferentiated, and the volume is such that a real defect -- an Apple API that +is deprecated today and deleted in two releases, a pointer/integer confusion in +generated code -- is invisible. Nothing in this tree has ever counted them. + +This tool splits a build log by ownership so each group can be driven to zero and +held there. Ownership comes from the manifest the translator writes +(cn1-source-manifest.txt); it cannot be derived from the path, because every one +of those four kinds ends up in the same flat directory. + +Held against a per-leg baseline, ratchet-style, in the manner of +scripts/check-cast-semantics.sh: the baseline records what was true the day the +gate went in, new entries are a failure, and an entry that stops reproducing must +be deleted rather than left to rot. + +Exit codes: + 0 no findings outside the baseline + 1 new findings, or stale baseline entries, or a diagnostic that could not be + attributed to any known origin + 2 the build this log came from did not compile everything the manifest lists, + so the census would undercount and must not be trusted +""" +import argparse +import json +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASELINE_DIR = os.path.join(ROOT, "scripts", "native-warnings") + +# --- Diagnostic grammars ---------------------------------------------------- +# +# Anchored at start of line so that source-snippet lines (" 1 | int x;") and +# caret lines (" | ^") never match: both begin with whitespace, and neither +# carries a ": warning: " at column zero. The clang-cl grammar is separate only +# because MSVC puts the location in parentheses; it still carries [-Wflag], +# because clang-cl is clang. +GNU_RE = re.compile( + r'^(?P[^\s][^:]*(?::[^:\s][^:]*)*?):(?P\d+):(?P\d+):\s+' + r'(?Pwarning|error|note):\s+(?P.*)$') +MSVC_RE = re.compile( + r'^(?P[A-Za-z]?[^\s(][^(]*)\((?P\d+)(?:,(?P\d+))?\):\s+' + r'(?Pwarning|error|note):\s+(?P.*)$') +BARE_RE = re.compile( + r'^(?:(?Pld|clang|clang\+\+|clang-cl|cc|gcc|ninja|xcodebuild|libtool)' + r':\s+)?(?Pwarning|error):\s+(?P.*)$') + +FLAG_RE = re.compile(r'\[(-W[^\]]+)\]\s*$') + +# Xcode names the source it is about to compile; ninja and make announce the +# object. Either way this is how we learn what the build ACTUALLY compiled, as +# opposed to what it could have compiled. +# Xcode names the task, the output, then the source: +# CompileC normal arm64 objective-c com.apple.compilers... (in target ...) +# Verified against Xcode 26.3 output. CompileMetalFile and the assembler use the +# same shape, and a .metal that only CompileC were matched would look like a +# source the build skipped. +COMPILE_XCODE_RE = re.compile( + r'^\s*(?:CompileC|CompileMetalFile|CompileAssembly)\s+(?:"[^"]*"|\S+)\s+' + r'(?P"[^"]+"|\S+)\s+normal\b') +# CMake's two generators announce the same thing with different progress +# prefixes -- ninja counts jobs ("[7/91]"), make counts percent ("[ 3%]") -- and +# the prefix is absent entirely when progress reporting is off. One optional group +# covers all three; splitting them into separate patterns is how the percent form +# got missed the first time, and a missed compile line reads as "the build +# compiled nothing", which is a hard failure rather than a wrong number. +COMPILE_CMAKE_RE = re.compile( + r'^\s*(?:\[\s*(?:\d+/\d+|\d+%)\s*\]\s*)?' + r'(?:Building|Compiling)\s+\S+\s+object\s+(?P\S+)') + +SOURCE_EXTS = (".m", ".mm", ".c", ".cc", ".cpp", ".cxx", ".metal", ".S", ".s") +HEADER_EXTS = (".h", ".hh", ".hpp") + +# Provenance rules for files the manifest does not name. These are rules about +# WHERE a file lives, not a list of files: a new SDK header or a new pod is +# covered without anyone editing this. +SDK_MARKERS = ( + "/Applications/Xcode", "/Library/Developer/", "/usr/include/", "/usr/lib/clang/", + ".sdk/", "/Toolchains/", "/usr/local/include/", "/MacOSX.platform/", + "/iPhoneOS.platform/", "/iPhoneSimulator.platform/", "/AppleTVOS.platform/", + "/WatchOS.platform/", "/lib/gcc/", "/mingw", "/msvc/", "/Windows Kits/", +) +VENDORED_MARKERS = ( + "/Pods/", "/SourcePackages/", "/Checkouts/", "/DerivedData/", + "/node_modules/", "/.build/", "/third_party/", "/xwin/", + # An .xcframework is unzipped into the build's own products directory before + # its headers are compiled against, so those headers arrive under a path that + # looks local and is not: TensorFlowLiteC's c_api.h reached the census this + # way. Not ours, and not something an application author can fix. + "/XCFrameworkIntermediates/", ".framework/Headers/", +) + +# A companion watchOS or tvOS app embedded in an iOS project is translated by a +# SECOND, independent ParparVM run, which writes its output to a sibling -src +# directory and its own manifest that nothing stages. Its files are generated code +# by exactly the same argument as the main app's, and a warning in one is a defect +# in the same emitter, so it is attributed the same way rather than left homeless. +COMPANION_SRC_MARKERS = ("/watch-src/", "/tv-src/") + +GATING_GROUPS = ("generated", "runtime", "port", "toolchain") +ALL_GROUPS = GATING_GROUPS + ("vendored", "sdk") + +# Port source directories per leg, MOST SPECIFIC FIRST. Used to turn a bare file +# name from the manifest back into a repo-relative path, so the baseline names the +# file a human has to open. +# +# Order matters and is not a tiebreak of convenience: METALView.m exists in both +# Ports/MacPort and Ports/iOSPort/nativeSources, and for a macOS build it is the +# MacPort one that was compiled. The leg knows which port it built, so listing that +# port first states a fact rather than guessing. A name that appears twice inside a +# SINGLE port tree is still a hard error -- there the leg tells us nothing and +# picking one would blame a file nobody edited. +LEG_PORT_DIRS = { + "ios-sim-debug": ["Ports/iOSPort/nativeSources"], + "ios-device-release": ["Ports/iOSPort/nativeSources"], + "macos": ["Ports/MacPort", "Ports/iOSPort/nativeSources"], + "windows-clang-cl": ["Ports/WindowsPort"], + "linux-cc": ["Ports/LinuxPort"], + "clean-target": [], +} + + +def signature(msg): + """Collapses a diagnostic message to its shape. + + 'unused variable locals_3_' and 'unused variable locals_17_' are one defect in + one emitter, not two findings, so the identifiers and numbers that differ + between instances are replaced by '?'. Without this the baseline would churn on + every commit that renumbered a local. + """ + msg = FLAG_RE.sub("", msg).strip() + msg = re.sub(r"'[^']*'", "?", msg) + msg = re.sub(r'"[^"]*"', "?", msg) + msg = re.sub(r'\b\d+(?:\.\d+)*\b', "?", msg) + return re.sub(r"\s+", " ", msg).strip() + + +class Diagnostic(object): + __slots__ = ("path", "line", "col", "flag", "msg", "group", "identity") + + def __init__(self, path, line, col, flag, msg): + # Normalised to forward slashes at construction. The Windows leg emits + # backslash paths and its log is read on whatever host runs the gate, so + # os.path.basename on a POSIX box would otherwise hand back the entire + # path and every Windows warning would be unattributable. + self.path = path.replace("\\", "/") + self.line = line + self.col = col + self.flag = flag + self.msg = msg + self.group = None + self.identity = None + + @property + def dedup_key(self): + # The warning's OWN location, never the translation unit that provoked it. + # A header warning is re-emitted once per including TU and once per + # architecture; those are one defect, and counting them per TU would make + # the number a function of how many files happen to include the header. + return (self.path, self.line, self.col, self.flag, self.msg) + + @property + def key(self): + return "|".join((self.group, self.identity, self.flag, signature(self.msg))) + + +# A line that is nothing but a path fragment: no whitespace, contains a slash, and +# carries no diagnostic of its own. xcodebuild occasionally breaks a long +# diagnostic across two lines at an arbitrary column, leaving the path prefix on +# one line and the rest on the next -- measured at 8 occurrences in 92,129 warnings +# on the iOS leg. Rare, but it produced file names like "odename1_ui_Display.m" +# (com_c + odename1_ui_Display.m), which belong to no file that exists and so could +# not be attributed to anyone. +SPLIT_PREFIX_RE = re.compile(r'^\S*/\S*$') + +# Lines that legitimately follow a diagnostic and must never be glued onto it: +# clang's source snippet and caret (both indented), the include-trace header, and +# a build task announcement. +# Lines that legitimately follow a diagnostic and must never be glued onto it: +# clang's source snippet (" 5646 | code") and its caret (" | ^"), +# the include-trace header, and a build task announcement. +# +# Excluding ALL indented lines here was wrong, and cost a red build: a split can +# land immediately before a space, leaving the continuation as " [-Wunused-variable]", +# which is indented and is exactly the thing that needs joining. Only the shapes +# that are genuinely something else are excluded; the join is still only made when +# it completes a trailing flag, so nothing else can be glued on by accident. +CONTINUATION_EXCLUDE_RE = re.compile( + r'^(?:\s*(?:\d+\s*)?\||In file included from\b|[A-Z][A-Za-z]+\s+/|\[\s*\d)') + + +def _parses(line): + return bool(GNU_RE.match(line) or MSVC_RE.match(line)) + + +def rejoin_split_lines(lines): + """Puts diagnostics back together that the log transport broke in two. + + xcodebuild's output reaches the log through a pipe, and a long diagnostic + occasionally arrives split at an arbitrary byte with the remainder on the next + line -- measured at roughly 18 occurrences in 92,129 warnings on the iOS leg. + It happens in two places, and both were found in real output rather than + imagined: + + - in the PATH, leaving "com_c" on one line and + "odename1_ui_Display.m:5646:5: warning: ..." on the next, which names a file + that does not exist and so belongs to nobody; + - in the MESSAGE, leaving "... warning: unuse" and then "d variable 'SP' + [-Wunused-variable]", which parses fine and produces a truncated message + shape -- a bogus baseline row that would never match again. + + Both joins are self-validating rather than guessed. A path join is only made + when the result parses as a diagnostic at all; a message join only when it + completes a trailing [-Wflag] that was absent before. Anything that does not + satisfy that is left exactly as it came. + """ + out = [] + i = 0 + n = len(lines) + while i < n: + line = lines[i] + nxt = lines[i + 1] if i + 1 < n else None + + if nxt is not None and not _parses(line) and SPLIT_PREFIX_RE.match(line) \ + and ": warning:" not in line and ": error:" not in line \ + and ": note:" not in line and _parses(line + nxt): + out.append(line + nxt) + i += 2 + continue + + if nxt is not None and _parses(line) and not FLAG_RE.search(line) \ + and not _parses(nxt) and not CONTINUATION_EXCLUDE_RE.match(nxt) \ + and FLAG_RE.search(line + nxt): + out.append(line + nxt) + i += 2 + continue + + out.append(line) + i += 1 + return out + + +def parse_log(text): + """Every warning in the log, deduplicated, plus the sources that were compiled.""" + seen = {} + order = [] + compiled = set() + lost = 0 + for raw in rejoin_split_lines([l.rstrip("\r") for l in text.splitlines()]): + line = raw + + m = COMPILE_XCODE_RE.match(line) + if m: + compiled.add(os.path.basename(m.group("src").strip('"'))) + else: + m = COMPILE_CMAKE_RE.match(line) + if m: + obj = os.path.basename(m.group("obj")) + # CMake names objects .o / .obj, so the source name + # is recoverable; anything else is left alone and simply will not + # match, which shows up as an incomplete build rather than silently + # passing. + for suffix in (".o", ".obj"): + if obj.endswith(suffix): + obj = obj[: -len(suffix)] + break + compiled.add(obj) + + m = GNU_RE.match(line) or MSVC_RE.match(line) + if m: + if m.group("sev") != "warning": + continue + msg = m.group("msg") + flag_m = FLAG_RE.search(msg) + d = Diagnostic(m.group("path"), int(m.group("line")), + int(m.group("col") or 0), + flag_m.group(1) if flag_m else "", msg) + else: + m = BARE_RE.match(line) + if not m or m.group("sev") != "warning": + continue + # A fileless diagnostic that nonetheless carries a [-Wflag] is not a + # build-system warning -- clang flags belong to file-scoped diagnostics. + # It is a compiler warning whose path the log transport dropped outright + # (bytes lost, not merely split, so nothing can put it back). Counting + # these keeps them visible; attributing them to the toolchain would be a + # lie, and baselining them would freeze a row that can never recur. + if FLAG_RE.search(m.group("msg")): + lost += 1 + continue + # No file and no flag: a linker or driver diagnostic. It still matters -- + # ThinLTO puts real findings here -- but it belongs to no source. + d = Diagnostic("", 0, 0, "", m.group("msg")) + + if d.dedup_key not in seen: + seen[d.dedup_key] = d + order.append(d) + return order, compiled, lost + + +def read_manifest(path): + """{file name: (origin, source)} from the translator's manifest.""" + entries = {} + with open(path, encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split("|") + if len(parts) != 3: + raise SystemExit("malformed manifest line in %s: %s" % (path, line)) + entries[parts[0]] = (parts[1], parts[2]) + return entries + + +_PORT_INDEX = {} + + +def port_index(leg): + """{file name: [repo-relative paths]} for the port trees this leg builds from. + + Built once per leg. The obvious spelling -- walk the tree looking for the name + each time a diagnostic needs resolving -- is O(diagnostics x files), and a real + census carries thousands of diagnostics. + """ + if leg in _PORT_INDEX: + return _PORT_INDEX[leg] + if leg not in LEG_PORT_DIRS: + # Silently treating an unknown leg as "no port trees" would resolve every + # hand-written native to nothing, reclassify all of it as vendored, and drop + # it out of the gating set -- the census would still print and still pass, + # having quietly stopped checking the code most worth checking. A leg with + # genuinely no port sources says so with an empty list, as clean-target does. + raise SystemExit( + "unknown leg %r: add it to LEG_PORT_DIRS naming the port trees it builds " + "from (an empty list if it has none). Known legs: %s" + % (leg, ", ".join(sorted(LEG_PORT_DIRS)))) + index = {} + for rel in LEG_PORT_DIRS[leg]: + base = os.path.join(ROOT, rel) + if not os.path.isdir(base): + continue + here = {} + for dirpath, _dirs, files in os.walk(base): + for fn in files: + here.setdefault(fn, []).append( + os.path.relpath(os.path.join(dirpath, fn), ROOT)) + # A more specific port tree already claimed this name; see LEG_PORT_DIRS. + for fn, paths in here.items(): + index.setdefault(fn, paths) + _PORT_INDEX[leg] = index + return index + + +def resolve_port_path(name, leg): + """Repo-relative path of a hand-written native, or None if it is not ours. + + Matched on file name because the origin recorded in the manifest is a staging + path inside whatever built the app, not a path in this checkout. Two matches is + a hard error rather than a guess: picking one would attribute a warning to a + file nobody edited. + """ + matches = port_index(leg).get(name, []) + if len(matches) > 1: + raise SystemExit( + "ambiguous port file %r for leg %s: %s\nResolve by scoping LEG_PORT_DIRS; " + "guessing would blame a file nobody edited." % (name, leg, ", ".join(sorted(matches)))) + return matches[0] if matches else None + + +def classify(diags, manifest, leg): + """Assigns every diagnostic an owner. Returns the ones that have no owner.""" + unattributed = [] + for d in diags: + if d.path == "": + d.group, d.identity = "toolchain", "" + continue + name = os.path.basename(d.path) + norm = d.path + entry = manifest.get(name) + if entry: + origin, _source = entry + if origin == "generated": + # Keyed on the emitter, not the file: a generated file exists only + # if its class survived elimination, and under concatenation there + # are no per-class files at all. + d.group, d.identity = "generated", "*" + elif origin in ("runtime", "vendored"): + d.group, d.identity = origin, name + elif origin == "port": + repo_path = resolve_port_path(name, leg) + # A hand-written native we cannot find in this checkout came from a + # cn1lib or the application, not from a port we maintain. + d.group = "port" if repo_path else "vendored" + d.identity = repo_path or name + else: + unattributed.append(d) + continue + if any(marker in norm for marker in COMPANION_SRC_MARKERS): + d.group, d.identity = "generated", "*" + continue + # Vendored is tested first because its markers are the more specific ones. + # "/Library/Developer/" matches a developer's own + # ~/Library/Developer/Xcode/DerivedData, so checking SDK first labelled a + # Swift package checkout as an Apple SDK header. Neither group gates, so + # this only ever affected what the report claimed -- but a census nobody + # believes is no better than no census. + if any(marker in norm for marker in VENDORED_MARKERS): + d.group, d.identity = "vendored", name + elif any(marker in norm for marker in SDK_MARKERS): + d.group, d.identity = "sdk", name + else: + unattributed.append(d) + return unattributed + + +def check_completeness(manifest, compiled): + """Sources the manifest lists that this build did not compile. + + An incremental build recompiles nothing and reports no warnings, which reads + exactly like a clean codebase; so does the documented xcodebuild failure where + a bad ARCHS override makes every target compile nothing while still copying + resources. Both are fatal to a census and both must be impossible to mistake + for progress. + + Measured against the manifest from the SAME build, deliberately. An earlier + version compared against the set of sources the baselined build compiled, and + two real runs of the same leg compiled 3147 and 3156 sources -- the app's + translated surface moves a little between runs, so a cross-run comparison + couples the gate to something that legitimately changes. Each of those runs + compiled 100% of its own manifest, which is the invariant that actually holds + and the one worth enforcing. + """ + expected = {n for n in manifest if n.endswith(SOURCE_EXTS)} + return sorted(expected - set(compiled)), sorted(expected) + + +def baseline_path(leg): + return os.path.join(BASELINE_DIR, "baseline-%s.txt" % leg) + + +def read_baseline(leg): + path = baseline_path(leg) + if not os.path.exists(path): + return set() + keys = set() + with open(path, encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line or line.startswith("#"): + continue + keys.add("|".join(line.split("|")[:4])) + return keys + + +def write_baseline(leg, diags, config): + path = baseline_path(leg) + counts = {} + for d in diags: + if d.group in GATING_GROUPS: + counts[d.key] = counts.get(d.key, 0) + 1 + with open(path, "w", encoding="utf-8") as fh: + fh.write("# Compiler warnings in the %s build, as of the day this gate was added.\n" % leg) + fh.write("#\n") + fh.write("# This is a ratchet, not an allow-list: new code must not add entries.\n") + fh.write("# Delete an entry when the warning stops reproducing -- a stale entry is a\n") + fh.write("# failure too, so the file cannot quietly describe a build nobody runs.\n") + fh.write("#\n") + fh.write("# A baseline is only comparable to a census taken from the SAME build. This\n") + fh.write("# one came from:\n") + for line in config.splitlines(): + fh.write("# %s\n" % line) + fh.write("#\n") + fh.write("# Format: ||||\n") + fh.write("#\n") + fh.write("# identity is a repo-relative path for port code, the file name for the\n") + fh.write("# runtime, and '*' for generated code -- there the unit of authorship is the\n") + fh.write("# emitter, not the file.\n") + fh.write("#\n") + fh.write("# A diagnostic cannot be -Wno-'d or -Werror='d on its own. The only\n") + fh.write("# remedies for one are fixing it or a whole-file pragma.\n") + fh.write("#\n") + fh.write("# Regenerate with scripts/check-native-warnings.py --leg %s --write-baseline\n" % leg) + fh.write("\n") + for key in sorted(counts): + fh.write("%s|%d instance(s) when the baseline was written; not yet triaged\n" + % (key, counts[key])) + return len(counts) + + +# Rows per group in the rendered table. The JSON dump carries everything; this +# only bounds what a human is asked to scroll, and keeps the GitHub step summary +# under its 1MB ceiling -- past which GitHub drops the whole summary, so an +# unbounded table would cost the entire report rather than its tail. +MAX_ROWS_PER_GROUP = 60 + + +def summarize(diags, out): + by_group = {} + for d in diags: + by_group.setdefault(d.group, []).append(d) + out.write("## Native warning census\n\n") + out.write("| group | distinct | instances | gating |\n") + out.write("|---|---|---|---|\n") + for g in ALL_GROUPS: + items = by_group.get(g, []) + distinct = len({d.key for d in items}) + out.write("| %s | %d | %d | %s |\n" + % (g, distinct, len(items), "yes" if g in GATING_GROUPS else "no")) + out.write("\n") + for g in ALL_GROUPS: + items = by_group.get(g, []) + if not items: + continue + # Grouped by the BASELINE key, not by (flag, shape): for the runtime and the + # ports the identity is the file, so the same flag in two files is two rows to + # fix and two rows in the baseline. Collapsing them here would make this table + # disagree with the "distinct" count above and understate the work. + counts = {} + files = {} + for d in items: + counts[d.key] = counts.get(d.key, 0) + 1 + files.setdefault(d.key, set()).add(os.path.basename(d.path)) + out.write("### %s\n\n" % g) + out.write("Sorted by instance count: the top row is the single highest-leverage fix.\n\n") + out.write("| instances | identity | flag | shape | seen in |\n") + out.write("|---|---|---|---|---|\n") + ranked = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])) + for key, n in ranked[:MAX_ROWS_PER_GROUP]: + _group, identity, flag, sig = key.split("|", 3) + names = sorted(files[key]) + shown = ", ".join(names[:3]) + (" (+%d more)" % (len(names) - 3) if len(names) > 3 else "") + out.write("| %d | %s | %s | %s | %s |\n" % (n, identity, flag, sig[:80], shown)) + if len(ranked) > MAX_ROWS_PER_GROUP: + hidden = ranked[MAX_ROWS_PER_GROUP:] + out.write("\n%d further row(s) not shown, %d instance(s) between them. " + "They are in the --json dump; this table is truncated for length, " + "not because the tail was dropped from the census.\n" + % (len(hidden), sum(n for _k, n in hidden))) + out.write("\n") + + +PROBE_FLAG = "-Wcn1-gate-probe" +PROBE_MESSAGE = "cn1 warning-gate probe" + + +def probe_target(manifest): + """A runtime file this leg actually has, to hang the probe warning on. + + Not a hard-coded name: the clean target writes the runtime out as + cn1_globals.c where the Apple targets keep cn1_globals.m, so any fixed + spelling is unattributable on some leg -- and an unattributable probe fails + for the wrong reason, which would look like the gate working when it is not. + """ + for name in sorted(manifest): + if manifest[name][0] == "runtime" and name.endswith(SOURCE_EXTS): + return name + raise SystemExit("--probe needs a runtime source in the manifest and found none") + +FIXTURE = os.path.join(BASELINE_DIR, "parser-fixture.log") + + +def self_test(): + """Proves the parser before any baseline exists, and needs no compiler. + + Every branch that has a way to be wrong gets a line: a flagged and an unflagged + diagnostic, the MSVC location format, the same header warning arriving from two + translation units and from two architectures (which must collapse to one), a + linker warning with no file, a note that must be dropped, and the snippet and + caret lines that must not be mistaken for diagnostics. + """ + with open(FIXTURE, encoding="utf-8") as fh: + diags, compiled, lost = parse_log(fh.read()) + got = {(os.path.basename(d.path), d.line, d.col, d.flag, signature(d.msg)) for d in diags} + expected = { + ("IOSNative.m", 4211, 9, "-Wdeprecated-declarations", + "? is deprecated: first deprecated in iOS ?"), + ("cn1_globals.m", 913, 12, "", "implicit declaration of function ?"), + ("CN1Vpn.m", 77, 5, "-Wunused-variable", "unused variable ?"), + ("cn1_globals.h", 2201, 30, "-Wsign-compare", + "comparison of integer expressions of different signedness"), + ("", 0, 0, "", + "object file was built for newer iOS version than being linked"), + ("com_codename1_ui_Form.m", 1502, 17, "-Wunused-variable", "unused variable ?"), + # Rejoined from a path split; the fragment alone named no real file. + ("com_codename1_ui_Display.m", 5646, 5, "-Wunused-variable", "unused variable ?"), + # Rejoined from a message split; unjoined this reads "unuse". + ("com_codename1_ui_Form.m", 912, 9, "-Wunused-variable", "unused variable ?"), + # A real fileless build-system warning, kept. + ("", 0, 0, "", + "Skipping duplicate build file in Compile Sources build phase"), + # Rejoined across an INDENTED continuation. Unjoined this keeps a truncated + # message and loses its flag, which is what reddened CI once. + ("com_codename1_ui_Button.m", 77, 9, "-Wunused-variable", "unused variable ?"), + # Genuinely unflagged, and its snippet/caret must NOT have been glued on. + ("cn1_globals.m", 42, 3, "", "implicit declaration of function ?"), + } + problems = [] + for extra in sorted(got - expected): + problems.append("parsed a line it should not have: %r" % (extra,)) + for missing in sorted(expected - got): + problems.append("failed to parse: %r" % (missing,)) + # The header diagnostic appears three times in the fixture -- twice from + # different TUs, once from a second architecture -- and must survive as one. + if lost != 1: + problems.append("expected exactly 1 truncation-lost diagnostic, got %d" % lost) + header = [d for d in diags if os.path.basename(d.path) == "cn1_globals.h"] + if len(header) != 1: + problems.append("header warning deduped to %d entries, expected 1" % len(header)) + if not {"IOSNative.m", "cn1_globals.c", "cn1_virtual_thread.c", + "CN1MetalShaders.metal"} <= compiled: + problems.append("did not recognise the compile lines: %s" % sorted(compiled)) + if problems: + for p in problems: + print("self-test: %s" % p, file=sys.stderr) + return 1 + print("self-test: %d diagnostics parsed, %d sources seen compiled -- OK" + % (len(diags), len(compiled))) + return 0 + + +def check_baselines(): + """Structural check on every checked-in baseline, needing no compiler. + + A baseline is the only record of what was true when the gate went in, and it is + edited by hand as warnings get fixed. These are the ways an edit goes wrong + without anyone noticing: a dropped field, an empty note (an entry nobody + justified), a group that never gates (so the row masks nothing and only + misleads), or a typo in the group name that silently stops matching. + """ + problems = [] + if not os.path.isdir(BASELINE_DIR): + return 0 + for fn in sorted(os.listdir(BASELINE_DIR)): + if not fn.startswith("baseline-") or not fn.endswith(".txt"): + continue + path = os.path.join(BASELINE_DIR, fn) + with open(path, encoding="utf-8") as fh: + for n, line in enumerate(fh, 1): + line = line.rstrip("\n") + if not line.strip() or line.startswith("#"): + continue + parts = line.split("|") + where = "%s:%d" % (fn, n) + if len(parts) != 5: + problems.append("%s: expected 5 |-separated fields, got %d: %s" + % (where, len(parts), line)) + continue + group, identity, flag, _sig, note = parts + if group not in ALL_GROUPS: + problems.append("%s: unknown group %r (one of %s)" + % (where, group, ", ".join(ALL_GROUPS))) + if group not in GATING_GROUPS: + problems.append("%s: group %r never gates, so this entry masks nothing " + "and only misleads -- delete it" % (where, group)) + if not identity.strip(): + problems.append("%s: empty identity" % where) + if not flag.startswith("-W") and flag != "": + problems.append("%s: %r is not a warning flag" % (where, flag)) + if not note.strip(): + problems.append("%s: empty note -- every baselined warning needs a " + "reason someone can read" % where) + if problems: + for p in problems: + print("baseline-check: %s" % p, file=sys.stderr) + return 1 + print("baseline-check: every checked-in baseline is well formed") + return 0 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--leg", help="which build this log came from, e.g. ios-sim-debug") + ap.add_argument("--log", help="the build log to read") + ap.add_argument("--manifest", help="cn1-source-manifest.txt from the same build") + ap.add_argument("--write-baseline", action="store_true", + help="record the current findings as the new baseline") + ap.add_argument("--report-only", action="store_true", + help="print the census and always exit 0") + ap.add_argument("--allow-partial", action="store_true", + help="do not require the build to have compiled everything (never in CI)") + ap.add_argument("--json", help="write every diagnostic to this file") + ap.add_argument("--self-test", action="store_true", help="check the parser against its fixture") + ap.add_argument("--check-baselines", action="store_true", + help="structural check on the checked-in baselines; needs no compiler") + ap.add_argument("--probe", action="store_true", + help="inject one synthetic warning and assert the gate reacts to it") + args = ap.parse_args() + + if args.self_test: + return self_test() or check_baselines() + if args.check_baselines: + return check_baselines() + + for required in ("leg", "log", "manifest"): + if not getattr(args, required): + ap.error("--%s is required" % required) + + with open(args.log, encoding="utf-8", errors="replace") as fh: + text = fh.read() + manifest = read_manifest(args.manifest) + probe_key = None + if args.probe: + target = probe_target(manifest) + text += "\n%s:1:1: warning: %s [%s]\n" % (target, PROBE_MESSAGE, PROBE_FLAG) + probe_key = "|".join(("runtime", target, PROBE_FLAG, PROBE_MESSAGE)) + + diags, compiled, lost = parse_log(text) + + never_compiled, expected = check_completeness(manifest, compiled) + if not args.allow_partial: + if not compiled: + print("FAIL: this log records no compilation at all, so an empty warning list " + "means nothing. A build that compiles nothing while still copying " + "resources looks exactly like this.", file=sys.stderr) + return 2 + if never_compiled: + print("FAIL: %d of the %d sources in this build's own manifest were never " + "compiled, so the census undercounts. Either the build was incremental " + "-- re-run it cold -- or a source has been excluded from the target, " + "in which case say so here rather than letting the count drift.\n %s%s" + % (len(never_compiled), len(expected), "\n ".join(never_compiled[:40]), + "\n ..." if len(never_compiled) > 40 else ""), file=sys.stderr) + return 2 + if lost: + print("note: %d diagnostic(s) lost their file to log truncation and are not " + "attributed to anyone; they are reported here and never baselined." % lost) + print("coverage: %d source(s) compiled, covering all %d in this build's manifest" + % (len(compiled), len(expected))) + + unattributed = classify(diags, manifest, args.leg) + if unattributed: + print("FAIL: %d diagnostics could not be attributed to any known origin. Every " + "warning has an owner; add the provenance rule rather than dropping " + "these.\n %s" % (len(unattributed), + "\n ".join(sorted({d.path for d in unattributed})[:20])), + file=sys.stderr) + return 1 + + if args.json: + with open(args.json, "w", encoding="utf-8") as fh: + json.dump([{"path": d.path, "line": d.line, "col": d.col, "flag": d.flag, + "message": d.msg, "group": d.group, "identity": d.identity, + "key": d.key} for d in diags], fh, indent=1, sort_keys=True) + + summarize(diags, sys.stdout) + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if step_summary: + with open(step_summary, "a", encoding="utf-8") as fh: + summarize(diags, fh) + + if args.write_baseline: + n = write_baseline(args.leg, diags, "leg: %s\nlog: %s" % (args.leg, os.path.basename(args.log))) + print("wrote %d baseline entries to %s" % (n, baseline_path(args.leg))) + return 0 + + gating = [d for d in diags if d.group in GATING_GROUPS] + baseline = read_baseline(args.leg) + seen_keys = {d.key for d in gating} + new = sorted(seen_keys - baseline) + stale = sorted(baseline - seen_keys) + + if args.probe: + # The probe asserts the whole chain reacted -- parse, classify, key, diff -- + # on the real log with the real manifest. A gate that has gone blind (missing + # manifest, wrong baseline path, everything silently bucketed as vendored) + # fails here on the day it breaks rather than the day someone notices it + # never fired. + if probe_key not in seen_keys: + print("FAIL: the injected probe warning did not come back as %r. The gate is " + "not classifying warnings correctly and is not gating." % probe_key, + file=sys.stderr) + return 1 + if probe_key not in new: + print("FAIL: the injected probe warning was not reported as new. The gate " + "would not fail on a newly introduced warning.", file=sys.stderr) + return 1 + print("probe: the gate reacted to an injected warning as expected") + return 0 + + if args.report_only: + print("\nreport-only: %d new, %d stale (not failing)" % (len(new), len(stale))) + return 0 + + status = 0 + if new: + print("\nFAIL: %d warning kind(s) are not in the baseline for %s:" % (len(new), args.leg), + file=sys.stderr) + for k in new: + print(" %s" % k, file=sys.stderr) + status = 1 + if stale: + print("\nFAIL: %d baseline entr(ies) for %s no longer reproduce. Delete them:" + % (len(stale), args.leg), file=sys.stderr) + for k in stale: + print(" %s" % k, file=sys.stderr) + status = 1 + if not status: + print("\nOK: %d gating diagnostic(s), all baselined (%d entries)." + % (len(gating), len(baseline))) + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check-native-warnings.sh b/scripts/check-native-warnings.sh new file mode 100755 index 00000000000..d75ca526ff5 --- /dev/null +++ b/scripts/check-native-warnings.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Classifies the compiler warnings in a native build log by who owns the code. +# +# A ParparVM build compiles four different kinds of C into one binary -- the +# translator's output, the ParparVM runtime, the hand-written port natives, and +# vendored third-party sources -- and their warnings arrive in one log with +# nothing to tell them apart. At that volume a real defect (an Apple API that is +# deprecated now and deleted in two releases, a pointer/integer confusion in +# generated code) is invisible. Ownership cannot be recovered from the path, +# because all four end up in the same flat directory; it comes from the manifest +# the translator writes beside the generated project. +# +# The parser self-test needs no compiler and is what PR CI runs: +# +# scripts/check-native-warnings.sh --self-test +# +# A census needs a log and the manifest from the SAME build: +# +# scripts/check-native-warnings.sh --leg ios-sim-debug \ +# --log artifacts/xcodebuild-build.log \ +# --manifest artifacts/cn1-source-manifest.txt +# +# Add --report-only to print the census without gating, --write-baseline to +# record the current findings, and --probe to assert the gate still reacts to an +# injected warning. +# +# Exit 2 means the build did not compile everything the manifest lists, so the +# census would undercount -- an incremental build reports no warnings and reads +# exactly like a clean codebase. Re-run against a cold build rather than +# believing the number. +set -euo pipefail + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +exec python3 "$SCRIPT_DIR/check-native-warnings.py" "$@" diff --git a/scripts/native-warnings/baseline-ios-sim-debug.txt b/scripts/native-warnings/baseline-ios-sim-debug.txt new file mode 100644 index 00000000000..24621db65d3 --- /dev/null +++ b/scripts/native-warnings/baseline-ios-sim-debug.txt @@ -0,0 +1,179 @@ +# Compiler warnings in the ios-sim-debug build, as of the day this gate was added. +# +# This is a ratchet, not an allow-list: new code must not add entries. +# Delete an entry when the warning stops reproducing -- a stale entry is a +# failure too, so the file cannot quietly describe a build nobody runs. +# +# A baseline is only comparable to a census taken from the SAME build. This +# one came from: +# leg: ios-sim-debug +# log: xcodebuild-build.log +# +# Format: |||| +# +# identity is a repo-relative path for port code, the file name for the +# runtime, and '*' for generated code -- there the unit of authorship is the +# emitter, not the file. +# +# A diagnostic cannot be -Wno-'d or -Werror='d on its own. The only +# remedies for one are fixing it or a whole-file pragma. +# +# Regenerate with scripts/check-native-warnings.py --leg ios-sim-debug --write-baseline + +generated|*|-Wimplicitly-unsigned-literal|integer literal is too large to be represented in a signed integer type, interpreting as unsigned|4 instance(s) when the baseline was written; not yet triaged +generated|*|-Wincompatible-pointer-types-discards-qualifiers|passing ? to parameter of type ? discards qualifiers|9094 instance(s) when the baseline was written; not yet triaged +generated|*|-Wparentheses-equality|equality comparison with extraneous parentheses|42 instance(s) when the baseline was written; not yet triaged +generated|*|-Wunused-variable|unused variable ?|50967 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/AudioPlayer.m|-Wincomplete-implementation|method definition for ? not found|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1AudioUnit.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1Camera.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use AVCaptureDeviceDiscoverySession instead.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1Crypto.m|-Warc-bridge-casts-disallowed-in-nonarc|? casts have no effect when not using ARC|10 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1Crypto.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1DragAndDrop.m|-Wunused-variable|unused variable ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|77 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wincompatible-pointer-types-discards-qualifiers|assigning to ? (aka ?) from ? (aka ?) discards qualifiers|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wint-conversion|incompatible pointer to integer conversion initializing ? (aka ?) with an expression of type ?|18 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wnonportable-include-path|non-portable path to file ?; specified path differs in case from file name on disk|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wpointer-sign|passing ? (aka ?) to parameter of type ? (aka ?) converts between pointers to integer types with different sign|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wunused-function|unused function ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1ES2compat.m|-Wunused-variable|unused variable ?|10 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1Language.m|-Wunused-function|unused function ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1SoundPool.m|-Wobjc-multiple-method-names|multiple methods named ? found|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m|-Wobjc-method-access|instance method ? not found (return type defaults to ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|5 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1TextInputView.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1VideoIO.m|-Wunused-function|unused function ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1Vision.m|-Wunused-function|unused function ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1WatchConnectivity.m|-Wunused-const-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CN1WatchViewController.m|-Wincomplete-implementation|method definition for ? not found|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/ClearRect.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|11 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/ClearRect.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/ClipRect.m|-Wunused-variable|unused variable ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m|-Wmismatched-parameter-types|conflicting parameter types in implementation of ?: ? vs ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m|-Wnonnull|null passed to a callee that requires a non-null argument|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|7 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|33 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - This method is unsafe because it could potentially cause buffer overruns. Use -getBytes:length: instead.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - UIActionSheet is deprecated. Use UIAlertController with a preferredStyle of UIAlertControllerStyleActionSheet instead|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - UIPopoverController is deprecated. Popovers are now implemented as UIViewController presentations. Use a modal presentation style of UIModalPresentationPopover and UIPopoverPresentationController.|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use -[UIDevice userInterfaceIdiom] directly.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use UIBarStyleBlack and set the translucent property to YES instead.|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use the block-based animation API instead|12 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use the interfaceOrientation property of the window scene instead.|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use the statusBarManager property of the window scene instead.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wdeprecated-declarations|? is deprecated: first deprecated in watchOS ? - This method is unsafe because it could potentially cause buffer overruns. Use -getBytes:length: instead.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wenum-conversion|implicit conversion from enumeration type ? (aka ?) to different enumeration type ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wenum-conversion|implicit conversion from enumeration type ? to different enumeration type ? (aka ?)|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wincompatible-pointer-types|incompatible pointer types initializing ? (aka ?) with an expression of type ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wint-conversion|incompatible pointer to integer conversion assigning to ? (aka ?) from ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wobjc-method-access|instance method ? not found (return type defaults to ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wobjc-missing-super-calls|method possibly missing a [super awakeFromNib] call|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wpointer-sign|passing ? (aka ?) to parameter of type ? converts between pointers to integer types with different sign|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|9 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m|-Wunused-variable|unused variable ?|12 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/Curve.c|-Wunused-function|unused function ?|8 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawGradient.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|19 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawGradient.m|-Wenum-conversion|implicit conversion from enumeration type ? to different enumeration type ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawGradient.m|-Wunused-const-variable|unused variable ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawGradient.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawImage.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|19 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawImage.m|-Wunused-const-variable|unused variable ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawImage.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawLine.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|13 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawLine.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawPath.m|-Wincompatible-pointer-types|incompatible pointer types assigning to ? (aka ?) from ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawRect.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|13 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawRect.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawString.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawString.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|19 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawString.m|-Wenum-conversion|implicit conversion from enumeration type ? to different enumeration type ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawString.m|-Wunused-const-variable|unused variable ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawString.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawStringTextureCache.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawStringTextureCache.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use -[UIDevice userInterfaceIdiom] directly.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/DrawTextureAlphaMask.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|29 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/EAGLView.h|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/EAGLView.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|26 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/EAGLView.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES is deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|6 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/ExecutableOp.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ? (aka ?)|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/FillPolygon.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|13 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/FillPolygon.m|-Wobjc-missing-super-calls|method possibly missing a [super dealloc] call|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/FillPolygon.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/FillRect.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|13 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/FillRect.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/GLUIImage.m|-Wenum-conversion|implicit conversion from enumeration type ? to different enumeration type ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/GLUIImage.m|-Wpointer-sign|passing ? to parameter of type ? (aka ?) converts between pointers to integer types with different sign|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wconditional-type-mismatch|pointer/integer type mismatch in conditional expression (? and ? (aka ?))|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|11 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - The segmentedControlStyle property no longer has any effect|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - UIActionSheet is deprecated. Use UIAlertController with a preferredStyle of UIAlertControllerStyleActionSheet instead|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - UIActionSheet is deprecated. Use UIAlertController with a preferredStyle of UIAlertControllerStyleActionSheet instead.|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - UIPopoverController is deprecated. Popovers are now implemented as UIViewController presentations. Use a modal presentation style of UIModalPresentationPopover and UIPopoverPresentationController.|6 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use -[UIDevice userInterfaceIdiom] directly.|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use -initWithBoundsSize:requestHandler:|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use -stringByAddingPercentEncodingWithAllowedCharacters: instead, which always uses the recommended UTF-? encoding, and which encodes for a specific URL component or subcomponent since each URL component or subcomponent has different rules for what characters are valid.|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use AVPlayerViewController in AVKit|50 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use AVPlayerViewController in AVKit.|46 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use UIBarStyleBlack and set the translucent property to YES instead.|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use appropriate initializers instead|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use kSecUseAuthenticationContext and set LAContext.localizedReason property|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use the interfaceOrientation property of the window scene instead.|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in watchOS ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in watchOS ? - Please use AVAudioApplication requestRecordPermissionWithCompletionHandler|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wdeprecated-declarations|? is deprecated: first deprecated in watchOS ? - Use kSecUseAuthenticationContext and set LAContext.localizedReason property|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wenum-conversion|implicit conversion from enumeration type ? (aka ?) to different enumeration type ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wextra-tokens|extra tokens at end of #endif directive|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wformat|values of type ? should not be used as format arguments; add an explicit cast to ? instead|6 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wincompatible-pointer-types-discards-qualifiers|initializing ? with an expression of type ? discards qualifiers|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wincompatible-pointer-types|incompatible pointer types assigning to ? (aka ?) from ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wincompatible-pointer-types|incompatible pointer types passing ? (aka ?) to parameter of type ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wint-conversion|incompatible integer to pointer conversion passing ? (aka ?) to parameter of type ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wint-conversion|incompatible pointer to integer conversion initializing ? (aka ?) with an expression of type ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wint-conversion|incompatible pointer to integer conversion passing ? to parameter of type ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wint-conversion|incompatible pointer to integer conversion returning ? from a function with result type ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wnonnull|null passed to a callee that requires a non-null argument|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wobjc-method-access|instance method ? not found (return type defaults to ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|7 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ? (aka ?)|6 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? to ?|28 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wtautological-pointer-compare|comparison of function ? not equal to a null pointer is always true|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wundeclared-selector|undeclared selector ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wunguarded-availability-new|? is only available on iOS ? or newer|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wunsupported-availability-guard|@available does not guard availability here; use if (@available) instead|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wunused-function|unused function ?|11 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m|-Wunused-variable|unused variable ?|13 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m||? may not respond to ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/IOSNative.m||assigning to ? from incompatible type ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.h|-Wnullability-completeness|pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified)|8 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Provide a custom network activity UI in your app if desired.|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Use NSURLSession (see NSURLSession.h)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wdeprecated-declarations|? is deprecated: first deprecated in watchOS ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wnonnull|null passed to a callee that requires a non-null argument|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/NetworkConnectionImpl.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? to ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/Renderer.c|-Wunused-function|unused function ?|4 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/ResetAffine.m|-Wobjc-designated-initializers|designated initializer missing a ? call to a designated initializer of the super class|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/SocketImpl.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/SocketImpl.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/TileImage.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - OpenGLES API deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)|19 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/TileImage.m|-Wunused-variable|unused variable ?|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m|-Wdeprecated-declarations|? is deprecated: first deprecated in iOS ? - Provide a custom network activity UI in your app if desired.|3 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ? (aka ?)|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m|-Wunused-variable|unused variable ?|1 instance(s) when the baseline was written; not yet triaged +port|Ports/iOSPort/nativeSources/WebSocketImpl.m|-Wunsupported-availability-guard|@available does not guard availability here; use if (@available) instead|6 instance(s) when the baseline was written; not yet triaged +runtime|cn1_globals.m|-Wformat|format specifies type ? but the argument has type ?|2 instance(s) when the baseline was written; not yet triaged +runtime|cn1_globals.m|-Wint-conversion|incompatible pointer to integer conversion assigning to ? (aka ?) from ?|2 instance(s) when the baseline was written; not yet triaged +runtime|cn1_globals.m|-Wpointer-to-int-cast|cast to smaller integer type ? (aka ?) from ? (aka ?)|2 instance(s) when the baseline was written; not yet triaged +runtime|cn1_globals.m|-Wunused-function|unused function ?|6 instance(s) when the baseline was written; not yet triaged +runtime|cn1_globals.m|-Wunused-variable|unused variable ?|8 instance(s) when the baseline was written; not yet triaged +runtime|nativeMethods.m|-Wpointer-to-int-cast|cast to smaller integer type ? (aka ?) from ? (aka ?)|4 instance(s) when the baseline was written; not yet triaged +runtime|nativeMethods.m|-Wshorten-64-to-32|implicit conversion loses integer precision: ? (aka ?) to ? (aka ?)|4 instance(s) when the baseline was written; not yet triaged +runtime|nativeMethods.m|-Wunused-variable|unused variable ?|2 instance(s) when the baseline was written; not yet triaged +toolchain|||Skipping duplicate build file in Compile Sources build phase: /Users/runner/work/CodenameOne/CodenameOne/scripts/hellocodenameone/ios/target/hellocodenameone-ios-?-SNAPSHOT-ios-source/HelloCodenameOne-src/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift (in target ? from project ?)|1 instance(s) when the baseline was written; not yet triaged diff --git a/scripts/native-warnings/parser-fixture.log b/scripts/native-warnings/parser-fixture.log new file mode 100644 index 00000000000..85f73561c1e --- /dev/null +++ b/scripts/native-warnings/parser-fixture.log @@ -0,0 +1,97 @@ +# Hand-authored input for scripts/check-native-warnings.py --self-test. +# +# One line for every branch of the parser that has a way to be wrong. Do not +# regenerate this from a real build: the point is that each line is here because +# somebody reasoned about it, and a real log would not reliably contain all of +# them at once. +# +# Not a real log format -- comments are simply lines the grammars do not match, +# which is itself worth asserting. + +CompileC /tmp/dd/Objects-normal/arm64/IOSNative.o /tmp/proj/HelloApp-src/IOSNative.m normal arm64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'HelloApp' from project 'HelloApp') +# A .metal goes through a DIFFERENT task. Matching only CompileC made it look +# like a source the build skipped, which is how "in the manifest but not in this +# target" got confused with "this build was incremental". +CompileMetalFile /tmp/dd/Metal/default.air /tmp/proj/HelloApp-src/CN1MetalShaders.metal normal arm64 metal com.apple.compilers.metal (in target 'HelloApp' from project 'HelloApp') +[1/9] Building C object CMakeFiles/HelloApp.dir/HelloApp-src/cn1_globals.c.o +# The SAME announcement from CMake's other generator. Make counts percent where +# ninja counts jobs; missing this form made a complete build look like one that +# compiled nothing, which the completeness guard reports as a hard failure. +[ 42%] Building C object CMakeFiles/HelloApp.dir/HelloApp-src/cn1_virtual_thread.c.o + +# 1. GNU format, flagged. The most common shape. +/tmp/proj/HelloApp-src/IOSNative.m:4211:9: warning: 'keyWindow' is deprecated: first deprecated in iOS 13.0 [-Wdeprecated-declarations] + +# 2. GNU format, NO flag. Cannot be -Wno-'d individually; must parse anyway. +/tmp/proj/HelloApp-src/cn1_globals.m:913:12: warning: implicit declaration of function 'cn1_missing_helper' + +# 3. MSVC/clang-cl location format. Still carries the clang flag. +C:\build\proj\HelloApp-src\CN1Vpn.m(77,5): warning: unused variable 'unusedLocal' [-Wunused-variable] + +# 4-6. The SAME header warning, from two translation units and then from a +# second architecture. Must dedupe to exactly one: it is one defect, and +# counting it per TU would make the number depend on how many files include it. +In file included from /tmp/proj/HelloApp-src/com_codename1_ui_Form.m:3: +/tmp/proj/HelloApp-src/cn1_globals.h:2201:30: warning: comparison of integer expressions of different signedness [-Wsign-compare] +In file included from /tmp/proj/HelloApp-src/com_codename1_ui_Label.m:3: +/tmp/proj/HelloApp-src/cn1_globals.h:2201:30: warning: comparison of integer expressions of different signedness [-Wsign-compare] +/tmp/proj/HelloApp-src/cn1_globals.h:2201:30: warning: comparison of integer expressions of different signedness [-Wsign-compare] + +# 7. A linker diagnostic with no source file at all. ThinLTO puts real findings +# here, so it has to be captured even though it belongs to no file. +ld: warning: object file was built for newer iOS version than being linked + +# 8. A note. Must be DROPPED, not attached to the warning above it: xcodebuild +# interleaves parallel target output, so a note is not reliably adjacent to the +# diagnostic it belongs to. +/tmp/proj/HelloApp-src/cn1_globals.h:2199:1: note: expanded from macro 'CN1_COMPARE' + +# 9. Two generated-code warnings whose only difference is the local's number. +# These must collapse to ONE baseline row: one emitter, one fix. +/tmp/proj/HelloApp-src/com_codename1_ui_Form.m:1502:17: warning: unused variable 'locals_3_' [-Wunused-variable] +/tmp/proj/HelloApp-src/com_codename1_ui_Form.m:1502:17: warning: unused variable 'locals_17_' [-Wunused-variable] + +# 10. A source snippet and its caret. Both start with whitespace and neither is +# a diagnostic; if either matched, every warning would be counted twice. + 1502 | JAVA_OBJECT locals_3_; + | ^ + +# --------------------------------------------------------------------------- +# Splits observed in a real iOS build log (92,129 warnings, ~18 split lines). +# xcodebuild's output reaches the log through a pipe and a long diagnostic can +# arrive broken at an arbitrary byte. Three shapes, all found in real output: + +# 11. Split inside the PATH. Alone, the second line names a file that does not +# exist and belongs to nobody; joined, it is com_codename1_ui_Display.m. +/tmp/proj/HelloApp-src/watch-src/com_c +odename1_ui_Display.m:5646:5: warning: unused variable 'SP' [-Wunused-variable] + +# 12. Split inside the MESSAGE. The first line parses on its own, which is worse +# than not parsing: it yields the message shape "unuse" and a baseline row that +# could never match again. +/tmp/proj/HelloApp-src/com_codename1_ui_Form.m:912:9: warning: unuse +d variable 'methodBlockOffset' [-Wunused-variable] + +# 13. Bytes LOST, not merely split -- the path and location are gone outright, so +# nothing can put this back. It must be counted as lost rather than attributed to +# the toolchain, and never baselined: a row keyed on no file can never recur. +warning: unused variable 'currentOffset' [-Wunused-variable] + +# 14. A genuine fileless build-system warning, which looks similar and must NOT be +# treated as truncation. It carries no [-Wflag], because clang flags belong to +# file-scoped diagnostics -- that is what separates the two. +warning: Skipping duplicate build file in Compile Sources build phase + +# 15. Split immediately BEFORE a space, so the continuation is indented. This is +# the shape that reddened CI: excluding every indented line as "probably a source +# snippet" also excluded this, and the diagnostic kept a truncated message with no +# flag. Distinguishing it from a real snippet is what the snippet/caret patterns +# are for; the join must still complete a flag. +/tmp/proj/HelloApp-src/com_codename1_ui_Button.m:77:9: warning: unused variable 'iterations' + [-Wunused-variable] + +# 16. A real snippet and caret pair following an UNFLAGGED diagnostic. Neither may +# be glued on, which is what separates 15 from noise. +/tmp/proj/HelloApp-src/cn1_globals.m:42:3: warning: implicit declaration of function 'cn1_absent' + 42 | cn1_absent(); + | ^ diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index 1e6e2dcfef5..210453ec129 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -702,6 +702,27 @@ fi CN1_TEST_OPT_LEVEL="${CN1_TEST_OPT_LEVEL:-2}" XCODE_BUILD_CMD+=("GCC_OPTIMIZATION_LEVEL=$CN1_TEST_OPT_LEVEL") ri_log "Building translated C at -O$CN1_TEST_OPT_LEVEL (GCC_OPTIMIZATION_LEVEL)" +# Warning census (CN1_WARNING_CENSUS=1, set by our workflows and by nothing a +# customer runs). The five settings below are OFF in the Xcode template, which +# quietens the translator's output at the cost of also blinding the ~85k lines of +# hand-written port natives compiled alongside it. Turning them back on for the +# census measures what restoring each would cost before any of them is changed in +# the template. +# +# These must be command-line overrides rather than an xcconfig: Xcode's precedence +# is command line > target > project > xcconfig, so an xcconfig saying YES loses to +# the project-level NO and would measure nothing at all -- a gate that reads +# nothing and reports success. +if [ "${CN1_WARNING_CENSUS:-0}" = "1" ]; then + ri_log "Warning census: re-enabling the warnings the template disables" + XCODE_BUILD_CMD+=( + "CLANG_WARN_EMPTY_BODY=YES" + "CLANG_WARN_ENUM_CONVERSION=YES" + "CLANG_WARN_INT_CONVERSION=YES" + "CLANG_WARN__DUPLICATE_METHOD_MATCH=YES" + "GCC_WARN_UNUSED_VARIABLE=YES" + ) +fi XCODE_BUILD_CMD+=(build) if ! "${XCODE_BUILD_CMD[@]}" | tee "$BUILD_LOG"; then # CI runners occasionally lose the booted device between simctl boot and the @@ -738,6 +759,42 @@ COMPILE_END=$(date +%s) COMPILATION_TIME=$((COMPILE_END - COMPILE_START)) ri_log "Compilation time: ${COMPILATION_TIME}s" +# Attribute this build's warnings to whoever owns the code and hold the result +# against the leg's baseline. A new warning kind fails here; so does a baselined +# one that has stopped reproducing, which is what keeps the file from drifting +# into a description of a build nobody runs. +# +# --probe re-runs the whole chain with one synthetic warning injected and asserts +# it comes back as new. That is what catches this gate going blind -- a missing +# manifest, a wrong baseline path, everything silently bucketed as vendored -- on +# the day it breaks rather than the day someone notices it never fired. +# +# The tool exits 2 on its own if this build compiled less than the one the +# baseline came from, so an incremental build cannot quietly report a small +# number and pass. +if [ "${CN1_WARNING_CENSUS:-0}" = "1" ]; then + CN1_WARNING_MANIFEST="${CN1_WARNING_MANIFEST:-$ARTIFACTS_DIR/cn1-source-manifest.txt}" + if [ ! -f "$CN1_WARNING_MANIFEST" ]; then + ri_log "STAGE:WARNING_CENSUS_FAILED -> no manifest at $CN1_WARNING_MANIFEST" + exit 12 + fi + CN1_WARNING_ARGS=( + --leg "${CN1_WARNING_LEG:-ios-sim-debug}" + --log "$BUILD_LOG" + --manifest "$CN1_WARNING_MANIFEST" + ) + if ! "$REPO_ROOT/scripts/check-native-warnings.sh" "${CN1_WARNING_ARGS[@]}" \ + --json "$ARTIFACTS_DIR/native-warnings.json"; then + ri_log "STAGE:WARNING_CENSUS_FAILED -> see the census output above" + exit 12 + fi + if ! "$REPO_ROOT/scripts/check-native-warnings.sh" "${CN1_WARNING_ARGS[@]}" --probe > /dev/null; then + ri_log "STAGE:WARNING_CENSUS_FAILED -> the gate did not react to an injected warning" + exit 12 + fi + ri_log "Warning census clean and the gate verified against an injected warning" +fi + BUILD_SETTINGS="$("$XCODEBUILD" "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -configuration Debug -showBuildSettings 2>/dev/null || true)" TARGET_BUILD_DIR="$(printf '%s\n' "$BUILD_SETTINGS" | awk -F' = ' '/ TARGET_BUILD_DIR /{print $2; exit}')" WRAPPER_NAME="$(printf '%s\n' "$BUILD_SETTINGS" | awk -F' = ' '/ WRAPPER_NAME /{print $2; exit}')" diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 585249e0c2e..bf5b813b1dc 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -127,6 +127,14 @@ boolean hasIosDeviceIdioms() { public static OutputType output = OutputType.OUTPUT_TYPE_IOS; public static boolean verbose = true; + + /** + * Provenance of every file that lands in the generated source directory, so a + * consumer of the build log can tell a codegen defect from a port bug from + * somebody else's vendored code. Written out beside the generated project at the + * end of each output handler; see {@link SourceManifest}. + */ + static final SourceManifest sourceManifest = new SourceManifest(); ByteCodeTranslator() { } @@ -172,6 +180,12 @@ void execute(File sourceDir, File outputDir) throws Exception { if(!f.isDirectory() && !isBuildMetadata(f)) { // copy the file to the dest dir copy(Files.newInputStream(f.toPath()), Files.newOutputStream(new File(outputDir, f.getName()).toPath())); + // Everything that reaches here is hand-written: a port native, a + // cn1lib's native, or an application resource. This is the only + // point at which its ORIGIN is still known -- one line further on + // it is an anonymous sibling of the generated code -- so record it + // here or lose the distinction for good. + sourceManifest.recordPort(f.getName(), f); } } } @@ -269,19 +283,69 @@ public static boolean isCheckedCastsEnabled() { /// #### Parameters /// /// - `srcRoot`: the directory the translated sources are written to + /** + * Copies one of the ParparVM runtime sources bundled in the translator's jar into + * the generated project, recording its provenance on the way through. + * + *

Every runtime file goes through here rather than calling {@link #copy} directly, + * so that one cannot be added later without being recorded. A file the manifest does + * not name is indistinguishable from generated code to anything reading the build + * log, which is exactly the confusion {@link SourceManifest} exists to prevent.

+ * + * @param srcRoot the generated project's source directory + * @param name the resource name, which is also the file name it is written under + * @return the file that was written, for callers that go on to edit it + */ + private static File copyRuntimeResource(File srcRoot, String name) throws IOException { + return copyRuntimeResource(srcRoot, name, name); + } + + /** + * As {@link #copyRuntimeResource(File, String)}, for the clean target, which writes + * several of the runtime sources out under a different name than the resource has -- + * {@code cn1_globals.m} becomes {@code cn1_globals.c}, because that target compiles C + * rather than Objective-C. The manifest records the name the file has ON DISK, since + * that is the name a compiler diagnostic will carry. + * + * @param srcRoot the generated project's source directory + * @param name the resource name inside the translator jar + * @param destName the file name to write it under + * @return the file that was written, for callers that go on to edit it + */ + private static File copyRuntimeResource(File srcRoot, String name, String destName) throws IOException { + File dest = new File(srcRoot, destName); + copy(ByteCodeTranslator.class.getResourceAsStream("/" + name), Files.newOutputStream(dest.toPath())); + sourceManifest.recordRuntime(destName, "/" + name); + return dest; + } + + /** + * As {@link #copyRuntimeResource}, for third-party code we bundle but do not + * maintain. Kept separate rather than taking an origin parameter because the two + * differ in what a warning MEANS: a warning in the runtime is ours to fix, and one + * in vendored code is reported and never gated. + * + * @param srcRoot the generated project's source directory + * @param name the resource name, which is also the file name it is written under + * @return the file that was written, for callers that go on to edit it + */ + private static File copyVendoredResource(File srcRoot, String name) throws IOException { + File dest = new File(srcRoot, name); + copy(ByteCodeTranslator.class.getResourceAsStream("/" + name), Files.newOutputStream(dest.toPath())); + sourceManifest.recordVendored(name, "/" + name); + return dest; + } + private static void emitBundledSqlite(File srcRoot) throws IOException { File sqliteUnity = new File(srcRoot, "cn1_sqlite3.c"); File sqliteHeader = new File(srcRoot, "cn1_sqlite3.h"); File sqliteAmalgamation = new File(srcRoot, "cn1_sqlite3_amalgamation.h"); File sqliteCipherMarker = new File(srcRoot, "cn1_sqlite3_cipher.h"); if (isBundledSqliteEnabled()) { - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_sqlite3.c"), - Files.newOutputStream(sqliteUnity.toPath())); + copyVendoredResource(srcRoot, "cn1_sqlite3.c"); replaceInFile(sqliteUnity, "//#define CN1_INCLUDE_SQLITE", "#define CN1_INCLUDE_SQLITE"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_sqlite3.h"), - Files.newOutputStream(sqliteHeader.toPath())); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_sqlite3_amalgamation.h"), - Files.newOutputStream(sqliteAmalgamation.toPath())); + copyVendoredResource(srcRoot, "cn1_sqlite3.h"); + copyVendoredResource(srcRoot, "cn1_sqlite3_amalgamation.h"); } else { deleteIfPresent(sqliteUnity); deleteIfPresent(sqliteHeader); @@ -293,8 +357,7 @@ private static void emitBundledSqlite(File srcRoot) throws IOException { // __has_include is the only thing they can agree on without per-target compiler // flags. Emitted only for an application that configures encryption, so everyone else // compiles the engine as plain SQLite and links no keying code at all. - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_sqlite3_cipher.h"), - Files.newOutputStream(sqliteCipherMarker.toPath())); + copyVendoredResource(srcRoot, "cn1_sqlite3_cipher.h"); } else { // Left behind, this would put the ciphers back into an engine emitted without them -- // __has_include does not care which run wrote the file. @@ -435,10 +498,8 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File b.execute(sources, srcRoot); - File cn1Globals = new File(srcRoot, "cn1_globals.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.h"), Files.newOutputStream(cn1Globals.toPath())); - File cn1Intrinsics = new File(srcRoot, "cn1_intrinsics.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_intrinsics.h"), Files.newOutputStream(cn1Intrinsics.toPath())); + File cn1Globals = copyRuntimeResource(srcRoot, "cn1_globals.h"); + copyRuntimeResource(srcRoot, "cn1_intrinsics.h"); // Virtual threads: the switch is a few instructions of assembly per // architecture, so the .S travels with the runtime rather than being // generated. A project that gets the C and not the .S links against a @@ -450,17 +511,12 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File if ("true".equalsIgnoreCase(System.getProperty("cn1.onDeviceDebug", "false"))) { replaceInFile(cn1Globals, "//#define CN1_ON_DEVICE_DEBUG", "#define CN1_ON_DEVICE_DEBUG"); } - File cn1GlobalsC = new File(srcRoot, "cn1_globals.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.m"), Files.newOutputStream(cn1GlobalsC.toPath())); - File nativeMethodsC = new File(srcRoot, "nativeMethods.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/nativeMethods.m"), Files.newOutputStream(nativeMethodsC.toPath())); + copyRuntimeResource(srcRoot, "cn1_globals.m", "cn1_globals.c"); + copyRuntimeResource(srcRoot, "nativeMethods.m", "nativeMethods.c"); if (System.getProperty("USE_RPMALLOC", "false").equals("true")) { - File malloc = new File(srcRoot, "malloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/malloc.c"), Files.newOutputStream(malloc.toPath())); - File rpmalloc = new File(srcRoot, "rpmalloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.c"), Files.newOutputStream(rpmalloc.toPath())); - File rpmalloch = new File(srcRoot, "rpmalloc.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.h"), Files.newOutputStream(rpmalloch.toPath())); + copyRuntimeResource(srcRoot, "malloc.c"); + copyRuntimeResource(srcRoot, "rpmalloc.c"); + copyRuntimeResource(srcRoot, "rpmalloc.h"); } // The bundled SQLite engine is emitted only for applications that actually use // com.codename1.db, so everyone else pays nothing for it. cn1_sqlite3.c is gated on @@ -469,26 +525,21 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File // Always emitted: it defines the native entry points either way, as real bindings when // the engine is present and as stubs when it is not, so an application that references // com.codename1.db links regardless of how the translator was invoked. - File sqliteBindings = new File(srcRoot, "cn1_db_sqlite_impl.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_db_sqlite_impl.h"), Files.newOutputStream(sqliteBindings.toPath())); + copyRuntimeResource(srcRoot, "cn1_db_sqlite_impl.h"); emitBundledSqlite(srcRoot); - File xmlvm = new File(srcRoot, "xmlvm.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/xmlvm.h"), Files.newOutputStream(xmlvm.toPath())); + copyRuntimeResource(srcRoot, "xmlvm.h"); // Win32 POSIX compatibility shim. Always emitted; both files are gated on // _WIN32 internally, so they compile to nothing on iOS/macOS/Linux and // provide pthreads/usleep/gettimeofday on Windows (clang-cl / MSVC ABI). - File cn1WinCompatH = new File(srcRoot, "cn1_win_compat.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_win_compat.h"), Files.newOutputStream(cn1WinCompatH.toPath())); - File cn1WinCompatC = new File(srcRoot, "cn1_win_compat.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_win_compat.c"), Files.newOutputStream(cn1WinCompatC.toPath())); + copyRuntimeResource(srcRoot, "cn1_win_compat.h"); + copyRuntimeResource(srcRoot, "cn1_win_compat.c"); Parser.writeOutput(srcRoot); File javaIoFileHeader = new File(srcRoot, "java_io_File.h"); if (javaIoFileHeader.exists()) { - File javaIoFileC = new File(srcRoot, "java_io_File_runtime.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/java_io_File.m"), Files.newOutputStream(javaIoFileC.toPath())); + copyRuntimeResource(srcRoot, "java_io_File.m", "java_io_File_runtime.c"); } File classMethodIndexM = new File(srcRoot, "cn1_class_method_index.m"); @@ -498,6 +549,11 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File if(!classMethodIndexM.delete()) { System.err.println("Deletion of " + classMethodIndexM.getAbsolutePath() + " failed"); } + // Parser recorded the .m it wrote; this target compiles C, so the file that + // actually reaches the compiler -- and that a diagnostic will name -- is the + // .c. Re-record under the surviving name and drop the one that no longer + // exists, or the manifest describes a file nothing will ever build. + sourceManifest.renameGenerated("cn1_class_method_index.m", "cn1_class_method_index.c"); } // Native Windows produces a single self-contained .exe (there is no .app @@ -518,6 +574,12 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File } writeCmakeProject(root, srcRoot, appName, appType); + + // Written to the project root rather than srcRoot on purpose: writeCmakeProject + // globs srcRoot for sources, and the Apple path lists it into the Xcode project, + // where an unrecognised extension lands in the resources phase and ships inside + // the bundle. See SourceManifest. + sourceManifest.write(root); } /** @@ -784,10 +846,8 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File b.execute(sources, srcRoot); - File cn1Globals = new File(srcRoot, "cn1_globals.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.h"), Files.newOutputStream(cn1Globals.toPath())); - File cn1Intrinsics = new File(srcRoot, "cn1_intrinsics.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_intrinsics.h"), Files.newOutputStream(cn1Intrinsics.toPath())); + File cn1Globals = copyRuntimeResource(srcRoot, "cn1_globals.h"); + copyRuntimeResource(srcRoot, "cn1_intrinsics.h"); // Virtual threads: the switch is a few instructions of assembly per // architecture, so the .S travels with the runtime rather than being // generated. A project that gets the C and not the .S links against a @@ -799,20 +859,14 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File if ("true".equalsIgnoreCase(System.getProperty("cn1.onDeviceDebug", "false"))) { replaceInFile(cn1Globals, "//#define CN1_ON_DEVICE_DEBUG", "#define CN1_ON_DEVICE_DEBUG"); } - File cn1GlobalsM = new File(srcRoot, "cn1_globals.m"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_globals.m"), Files.newOutputStream(cn1GlobalsM.toPath())); - File nativeMethods = new File(srcRoot, "nativeMethods.m"); - copy(ByteCodeTranslator.class.getResourceAsStream("/nativeMethods.m"), Files.newOutputStream(nativeMethods.toPath())); - File javaIoFileM = new File(srcRoot, "java_io_File.m"); - copy(ByteCodeTranslator.class.getResourceAsStream("/java_io_File.m"), Files.newOutputStream(javaIoFileM.toPath())); + copyRuntimeResource(srcRoot, "cn1_globals.m"); + copyRuntimeResource(srcRoot, "nativeMethods.m"); + copyRuntimeResource(srcRoot, "java_io_File.m"); if (System.getProperty("USE_RPMALLOC", "false").equals("true")) { - File malloc = new File(srcRoot, "malloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/malloc.c"), Files.newOutputStream(malloc.toPath())); - File rpmalloc = new File(srcRoot, "rpmalloc.c"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.c"), Files.newOutputStream(rpmalloc.toPath())); - File rpmalloch = new File(srcRoot, "rpmalloc.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/rpmalloc.h"), Files.newOutputStream(rpmalloch.toPath())); + copyRuntimeResource(srcRoot, "malloc.c"); + copyRuntimeResource(srcRoot, "rpmalloc.c"); + copyRuntimeResource(srcRoot, "rpmalloc.h"); } // The bundled SQLite engine is emitted only for applications that actually use // com.codename1.db, so everyone else pays nothing for it. cn1_sqlite3.c is gated on @@ -821,8 +875,7 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File // Always emitted: it defines the native entry points either way, as real bindings when // the engine is present and as stubs when it is not, so an application that references // com.codename1.db links regardless of how the translator was invoked. - File sqliteBindings = new File(srcRoot, "cn1_db_sqlite_impl.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/cn1_db_sqlite_impl.h"), Files.newOutputStream(sqliteBindings.toPath())); + copyRuntimeResource(srcRoot, "cn1_db_sqlite_impl.h"); emitBundledSqlite(srcRoot); Parser.writeOutput(srcRoot); @@ -833,8 +886,7 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File File templatePch = new File(srcRoot, appName + "-Prefix.pch"); copy(ByteCodeTranslator.class.getResourceAsStream(templateRoot + "/template/template-Prefix.pch"), Files.newOutputStream(templatePch.toPath())); - File xmlvm = new File(srcRoot, "xmlvm.h"); - copy(ByteCodeTranslator.class.getResourceAsStream("/xmlvm.h"), Files.newOutputStream(xmlvm.toPath())); + copyRuntimeResource(srcRoot, "xmlvm.h"); File projectWorkspaceData = new File(projectXCworkspace, "contents.xcworkspacedata"); copy(ByteCodeTranslator.class.getResourceAsStream(templateRoot + "/template.xcodeproj/project.xcworkspace/contents.xcworkspacedata"), Files.newOutputStream(projectWorkspaceData.toPath())); @@ -1066,6 +1118,12 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File String bundleVersion = System.getProperty("bundleVersionNumber", appVersion); replaceInFile(templateInfoPlist, "com.codename1pkg", appPackageName, "${PRODUCT_NAME}", appDisplayName, "VERSION_VALUE", appVersion, "VERSION_BUNDLE_VALUE", bundleVersion); + + // Written to the project root, NOT to srcRoot. srcRoot.list() above feeds the + // Xcode project, and getFileType() has no case for .txt, so a manifest left in + // srcRoot would fall through to ***RESOURCES*** and be copied inside the shipped + // .app. See SourceManifest. + sourceManifest.write(root); } private static void writeCmakeProject(File projectRoot, File srcRoot, String appName, String appType) throws IOException { @@ -1570,6 +1628,7 @@ private static void emitVirtualThreadRuntime(File srcRoot) throws IOException { throw new IOException("virtual-thread runtime resource missing: " + name); } copy(in, Files.newOutputStream(new File(srcRoot, name).toPath())); + sourceManifest.recordRuntime(name, "/" + name); } } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index d462fe03d28..1d155f7180f 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -684,9 +684,11 @@ private static void generateClassAndMethodIndexHeader(File outputDirectory) thro FileOutputStream fos = new FileOutputStream(new File(outputDirectory, "cn1_class_method_index.h")); fos.write(bld.toString().getBytes(StandardCharsets.UTF_8)); fos.close(); + ByteCodeTranslator.sourceManifest.recordGenerated("cn1_class_method_index.h"); fos = new FileOutputStream(new File(outputDirectory, "cn1_class_method_index.m")); fos.write(bldM.toString().getBytes(StandardCharsets.UTF_8)); fos.close(); + ByteCodeTranslator.sourceManifest.recordGenerated("cn1_class_method_index.m"); } private static String encodeString(String con) { @@ -1158,6 +1160,13 @@ private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFi if (outMain instanceof ConcatenatingFileOutputStream) { ((ConcatenatingFileOutputStream)outMain).beginNextFile(cls.getClsName()); + } else { + // Only the one-file-per-class case has a file name worth recording. Under + // concatenation the classes are bucketed into concatenated_ and the + // provenance of an individual class is genuinely gone by the time the + // compiler sees it; the manifest says so by simply not naming these. + ByteCodeTranslator.sourceManifest.recordGenerated( + cls.getClsName() + "." + ByteCodeTranslator.output.extension()); } if (ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_JAVASCRIPT) { outMain.write(cls.generateJavascriptCode(classes).getBytes(StandardCharsets.UTF_8)); @@ -1171,6 +1180,10 @@ private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFi try(FileOutputStream outHeader = new FileOutputStream(new File(outputDir, headerName))) { outHeader.write(cls.generateCHeader().getBytes(StandardCharsets.UTF_8)); } + // The header is per-class even when the bodies are concatenated, so it is + // always recordable -- and it is where a good share of the generated-code + // diagnostics land, since every class that depends on this one includes it. + ByteCodeTranslator.sourceManifest.recordGenerated(headerName); } } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/SourceManifest.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/SourceManifest.java new file mode 100644 index 00000000000..49b51db0569 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/SourceManifest.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.tools.translator; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Records where every file in the generated project's source directory came from. + * + *

Why this has to exist. The translator drops four completely different + * kinds of C into one flat directory: the code it generated itself, the ParparVM + * runtime it copied out of its own jar, the hand-written port natives, and vendored + * third-party sources such as the bundled SQLite amalgamation. After + * {@link ByteCodeTranslator#execute} has run they are siblings with nothing to tell + * them apart -- {@code CN1Vision.m} and {@code com_codename1_ui_Form.m} sit in the + * same directory and differ only in who wrote them. A compiler warning in the first + * is ours to fix today; one in the second is a codegen defect; one in the third is + * somebody else's code we cannot touch. Without a record written at copy time that + * distinction is simply gone, and a warning report over the build log can only + * lump them together.

+ * + *

The translator is the only component that ever knows, because it is the thing + * doing the copying, so it writes what it knows to {@value #FILE_NAME} beside the + * generated project.

+ * + *

The file must not live in the source directory it describes. + * {@code handleAppleOutput} lists that directory and puts everything it finds into + * the Xcode project; {@code getFileType} has no case for {@code .txt}, so an entry + * would fall through to the resources build phase and the manifest would be copied + * inside the shipped {@code .app}. It is written to the project root instead -- + * the parent -- which nothing enumerates.

+ * + *

Entries are keyed by file name rather than by path on purpose: the name is what + * survives into the generated project, into the compiler's command line, and into + * the diagnostics in the build log, which is where this data is consumed. Recording + * the same name twice is not an error -- a later copy legitimately overwrites an + * earlier one -- and the last writer wins, matching what the filesystem did.

+ */ +public class SourceManifest { + /** Name of the manifest, written to the generated project's root directory. */ + public static final String FILE_NAME = "cn1-source-manifest.txt"; + + /** + * Who wrote a file. Ordered from "ours and generated" to "not ours at all", + * which is also the order of how much a warning in one of them means. + */ + public enum Origin { + /** + * Emitted by the translator from Java bytecode. A warning here is a defect in + * an emitter, and fixing one emitter fixes every file it wrote. + */ + GENERATED("generated"), + + /** + * The ParparVM runtime, copied verbatim out of the translator's own jar + * ({@code cn1_globals.m}, {@code nativeMethods.m} and friends). Hand-written + * and ours. + */ + RUNTIME("runtime"), + + /** + * A hand-written native from one of the ports, or from a cn1lib. Ours to fix + * when it is a port file; the consumer resolves which by looking the name up + * in the checked-out tree. + */ + PORT("port"), + + /** + * Third-party code we bundle but do not maintain, such as the SQLite + * amalgamation. Reported, never gated. + */ + VENDORED("vendored"); + + private final String token; + + Origin(String token) { + this.token = token; + } + + /** + * How this origin is spelled in the manifest. + * + *

A literal, not {@code name().toLowerCase()}. Case folding is locale + * sensitive and Codename One has no {@code java.util.Locale} to ask for the + * root one, so the fold a device performs depends on who is holding it -- and + * this is a protocol token another program parses back, which is exactly the + * case that must never be produced by folding. Writing it out also means the + * wire format is stated here rather than being an accident of the Java + * identifier, so renaming the constant cannot silently change the file.

+ * + * @return the lower-case ASCII token, identical on every device + */ + public String token() { + return token; + } + } + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + /** + * Insertion-ordered so the manifest reads in the order the build produced it, + * which makes a diff between two builds meaningful. + */ + private final Map entries = new LinkedHashMap(); + + /** One recorded file. */ + public static final class Entry { + private final String name; + private final Origin origin; + private final String source; + + Entry(String name, Origin origin, String source) { + this.name = name; + this.origin = origin; + this.source = source; + } + + public String getName() { + return name; + } + + public Origin getOrigin() { + return origin; + } + + /** + * Where the file came from -- an absolute path for a copied file, a + * {@code resource:} URL for one unpacked from the translator jar, or the empty + * string for generated code, which has no prior existence. + */ + public String getSource() { + return source; + } + } + + /** + * Records a file the translator generated. {@code name} is the file name as it + * appears in the project directory. + */ + public void recordGenerated(String name) { + record(name, Origin.GENERATED, ""); + } + + /** Records a runtime file unpacked from the translator's own jar. */ + public void recordRuntime(String name, String resourceName) { + record(name, Origin.RUNTIME, "resource:" + resourceName); + } + + /** Records a vendored third-party file unpacked from the translator's own jar. */ + public void recordVendored(String name, String resourceName) { + record(name, Origin.VENDORED, "resource:" + resourceName); + } + + /** + * Records a hand-written native copied in from the application's or a port's + * source tree. + */ + public void recordPort(String name, File origin) { + record(name, Origin.PORT, origin == null ? "" : origin.getAbsolutePath()); + } + + private void record(String name, Origin origin, String source) { + if (name == null || name.isEmpty()) { + return; + } + entries.put(name, new Entry(name, origin, source)); + } + + /** + * Re-records an entry under a new file name, keeping its origin. + * + *

For the case where a later stage renames a file the translator already wrote -- + * the clean target copies {@code cn1_class_method_index.m} to {@code .c} and deletes + * the original, because it compiles C rather than Objective-C. The manifest has to + * name the file that reaches the compiler, since that is the name a diagnostic will + * carry; leaving the old name behind would describe a file nothing builds and hide + * the one that is built.

+ * + *

Does nothing when {@code fromName} was never recorded, so a caller does not have + * to know whether the rename it is reporting actually happened.

+ * + * @param fromName the name the file was recorded under + * @param toName the name it now has on disk + */ + public void renameGenerated(String fromName, String toName) { + Entry existing = entries.remove(fromName); + if (existing == null) { + return; + } + entries.put(toName, new Entry(toName, existing.origin, existing.source)); + } + + /** Every entry, in the order it was recorded. */ + public List getEntries() { + return Collections.unmodifiableList(new ArrayList(entries.values())); + } + + /** The recorded origin of {@code name}, or null when it was never recorded. */ + public Origin originOf(String name) { + Entry e = entries.get(name); + return e == null ? null : e.origin; + } + + public int size() { + return entries.size(); + } + + /** + * Writes the manifest to {@link #FILE_NAME} in {@code projectRoot}. + * + *

Pass the project root, never the source directory: see the note on the + * class about the manifest otherwise shipping inside the application bundle.

+ */ + public void write(File projectRoot) throws IOException { + File out = new File(projectRoot, FILE_NAME); + try (Writer w = new OutputStreamWriter(Files.newOutputStream(out.toPath()), UTF8)) { + w.write("# Provenance of every file in the generated project's source directory.\n"); + w.write("# Written by the ParparVM translator; consumed by\n"); + w.write("# scripts/check-native-warnings.py to decide who owns a compiler warning.\n"); + w.write("#\n"); + w.write("# Format: ||\n"); + w.write("# origin is one of: generated, runtime, port, vendored\n"); + w.write("#\n"); + w.write("# generated has no source: it did not exist before this build.\n"); + w.write("\n"); + for (Entry e : entries.values()) { + w.write(e.name); + w.write('|'); + w.write(e.origin.token()); + w.write('|'); + w.write(e.source); + w.write('\n'); + } + } + } +} 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 index c6a027bab47..7dcd91d594a 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -264,6 +264,122 @@ void generatesRunnableExecutableForWindowsAppType(CompilerHelper.CompilerConfig * environment. This is a compile check (clang-cl /c) -- linking the full app * is exercised separately once a CN1 app translation is wired. */ + /** + * The translator records where every file in the generated source directory came + * from, so that a later pass over a compiler's output can tell a codegen defect + * from a port bug from vendored third-party code. Without this the four kinds are + * indistinguishable siblings in one flat directory. + * + *

Three things are asserted, and each of them has already been wrong once:

+ *
    + *
  • The manifest is complete. Every source in the directory is named by + * it, and it names nothing that is not there. A copy path added later without + * a matching recorder shows up here as an unrecorded file; a file renamed + * after it was recorded (the clean target rewrites + * {@code cn1_class_method_index.m} to {@code .c}) shows up as a phantom.
  • + *
  • All three of our origins are populated. A manifest that classified + * everything as one origin would pass a completeness check and be useless.
  • + *
  • It is NOT inside the source directory. The Apple path lists that + * directory into the Xcode project and {@code getFileType} has no case for + * {@code .txt}, so a manifest left there lands in the resources build phase + * and is copied inside the shipped {@code .app}.
  • + *
+ */ + @org.junit.jupiter.api.Test + void recordsSourceProvenanceOutsideTheSourceDirectory() throws Exception { + java.util.List configs = new java.util.ArrayList<>(); + for (String v : new String[] { "17", "21", "25", "11", "1.8" }) { + configs.addAll(CompilerHelper.getAvailableCompilers(v)); + } + org.junit.jupiter.api.Assumptions.assumeFalse(configs.isEmpty(), "No JDK available to translate with"); + CompilerHelper.CompilerConfig config = configs.get(0); + + Parser.cleanup(); + Path sourceDir = Files.createTempDirectory("manifest-sources"); + Path classesDir = Files.createTempDirectory("manifest-classes"); + Path javaApiDir = Files.createTempDirectory("manifest-japi"); + Path javaFile = sourceDir.resolve("HelloWorld.java"); + Files.write(javaFile, helloWorldSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("native_hello.c"), nativeHelloSource().getBytes(StandardCharsets.UTF_8)); + + CompilerHelper.compileJavaAPI(javaApiDir, config); + List compileArgs = new java.util.ArrayList<>(); + if (CompilerHelper.useClasspath(config)) { + compileArgs.add("-source"); compileArgs.add(config.targetVersion); + compileArgs.add("-target"); compileArgs.add(config.targetVersion); + compileArgs.add("-classpath"); compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-source"); compileArgs.add(config.targetVersion); + compileArgs.add("-target"); compileArgs.add(config.targetVersion); + compileArgs.add("-bootclasspath"); compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); compileArgs.add(classesDir.toString()); + compileArgs.add(javaFile.toString()); + assertEquals(0, CompilerHelper.compile(config.jdkHome, compileArgs), "HelloWorld should compile"); + CompilerHelper.copyDirectory(javaApiDir, classesDir); + // A hand-written native, so the "port" origin has something in it. This is the + // file whose provenance is lost the instant it is copied next to the generated + // code, which is the whole reason the manifest exists. + Files.copy(sourceDir.resolve("native_hello.c"), classesDir.resolve("native_hello.c")); + + Path outputDir = Files.createTempDirectory("manifest-output"); + runTranslator(classesDir, outputDir, "ManifestApp", "clean"); + + Path distDir = outputDir.resolve("dist"); + Path srcRoot = distDir.resolve("ManifestApp-src"); + Path manifest = distDir.resolve("cn1-source-manifest.txt"); + + assertTrue(Files.exists(manifest), "translator should write " + manifest); + assertFalse(Files.exists(srcRoot.resolve("cn1-source-manifest.txt")), + "the manifest must not sit in the source directory: the Apple path lists that " + + "directory into the Xcode project and would ship the manifest inside the .app"); + + java.util.Map originByName = new java.util.HashMap<>(); + for (String line : Files.readAllLines(manifest, StandardCharsets.UTF_8)) { + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + String[] parts = line.split("\\|", -1); + assertEquals(3, parts.length, "malformed manifest line: " + line); + originByName.put(parts[0], parts[1]); + } + + java.util.Set onDisk = new java.util.TreeSet<>(); + try (java.util.stream.Stream files = Files.list(srcRoot)) { + files.filter(Files::isRegularFile) + .map(f -> f.getFileName().toString()) + .filter(n -> n.endsWith(".c") || n.endsWith(".m") || n.endsWith(".h") || n.endsWith(".S")) + .forEach(onDisk::add); + } + assertFalse(onDisk.isEmpty(), "the translation produced no sources at all"); + + java.util.Set unrecorded = new java.util.TreeSet<>(onDisk); + unrecorded.removeAll(originByName.keySet()); + assertTrue(unrecorded.isEmpty(), + "these sources are in the generated project but the manifest does not name them, " + + "so nothing can tell who owns a warning in them -- a copy path was probably " + + "added without a recorder: " + unrecorded); + + java.util.Set phantom = new java.util.TreeSet<>(); + for (String name : originByName.keySet()) { + if ((name.endsWith(".c") || name.endsWith(".m") || name.endsWith(".h") || name.endsWith(".S")) + && !onDisk.contains(name)) { + phantom.add(name); + } + } + assertTrue(phantom.isEmpty(), + "the manifest names sources that are not in the generated project; something " + + "renamed or removed a file after recording it: " + phantom); + + java.util.Set origins = new java.util.TreeSet<>(originByName.values()); + assertTrue(origins.contains("generated"), "no generated code was recorded: " + origins); + assertTrue(origins.contains("runtime"), "no ParparVM runtime was recorded: " + origins); + assertTrue(origins.contains("port"), "no hand-written native was recorded: " + origins); + assertEquals("port", originByName.get("native_hello.c"), + "the hand-written native must not be classified as generated code"); + } + @org.junit.jupiter.api.Test void compilesWindowsPortNativeLayer() throws Exception { org.junit.jupiter.api.Assumptions.assumeTrue(CompilerHelper.isWindows(),