Add code graph generation and verification to build process and documentation
diff --git a/README.md b/README.md index e0123e8..82e85e2 100644 --- a/README.md +++ b/README.md
@@ -259,6 +259,49 @@ - `option-with-sass-compile`: The Themes contain a lot of Modules, where each contains SASS resources. Compiling all of these on every build would only slow down the build. Especially because the SASS resources are only changed very infrequently. Therefore, the compiled CSS files are checked in. When enabling this profile the checked in CSS resources are replaced by freshly compiled versions, which then can be checked in. - `option-with-swf`: Per default the Royale build only builds the JavaScript-only version. This can be used to build web-based applications. If you however wish to build Flash and Air versions, you need to activate this profile in order for also building the Flash enabled versions of all libraries. +# Code Graphs + +Royale can generate deterministic JSON code graphs of the public framework API for both JavaScript and SWF targets. The graphs contain public types and members, ASDoc, metadata, inheritance and reference relationships, target-specific APIs, Maven dependencies, and MXML tag mappings. + +Generate and validate every framework module with Ant: + +```bash +ant -f frameworks/build.xml codegraphs +node frameworks/scripts/validate-codegraphs.js +``` + +Generate one module, or only rebuild the aggregate indexes: + +```bash +ant -Dcodegraph.module=Basic -f frameworks/build.xml codegraph +ant -f frameworks/build.xml codegraph-index +``` + +The Maven build provides the same graphs through the opt-in `codegraphs` profile. Include `option-with-swf` when building the framework modules so that both target artifacts are available: + +```bash +./mvnw -f frameworks/projects/pom.xml -Pcodegraphs,option-with-swf -DskipTests prepare-package +./mvnw -f distribution/pom.xml -Pcodegraphs -DskipTests verify +``` + +The distribution build attaches the graphs as: + +```text +org.apache.royale.framework:distribution:zip:codegraphs:<version> +``` + +Binary SDK and npm distributions place the aggregate tree in `frameworks/codegraphs`. The Maven classifier has the same tree at the archive root. `index.json` lists module coordinates, target dependencies, shard paths, counts, and SHA-256 hashes. `mxml.json` maps MXML namespaces and tags to graph symbols. Graph shards use this layout: + +```text +<version>/<module>/<js|swf>/<module>.json +``` + +To verify an extracted aggregate tree independently, run: + +```bash +node frameworks/scripts/verify-codegraph-package.js --root <path-to-codegraphs> +``` + # Using Royale In order to get started using Royale, you are invited to follow along with the [Quick Start Guide](https://github.com/apache/royale-asjs/wiki/Quick-Start).
diff --git a/distribution/pom.xml b/distribution/pom.xml index 56bb930..6814ebf 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml
@@ -704,6 +704,43 @@ </target> </configuration> </execution> + <execution> + <id>verify-codegraph-artifact</id> + <phase>verify</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <target> + <delete dir="${project.build.directory}/codegraphs-verification"/> + <unzip src="${project.build.directory}/${distributionFileName}-codegraphs.zip" + dest="${project.build.directory}/codegraphs-verification"/> + <exec executable="node" failonerror="true"> + <arg value="${project.basedir}/../frameworks/scripts/verify-codegraph-package.js"/> + <arg value="--root"/> + <arg value="${project.build.directory}/codegraphs-verification"/> + </exec> + </target> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>attach-codegraphs</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>src/main/assembly/codegraphs.xml</descriptor> + </descriptors> + </configuration> + </execution> </executions> </plugin> </plugins>
diff --git a/distribution/src/main/assembly/codegraphs.xml b/distribution/src/main/assembly/codegraphs.xml new file mode 100644 index 0000000..618e06f --- /dev/null +++ b/distribution/src/main/assembly/codegraphs.xml
@@ -0,0 +1,37 @@ +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +--> + +<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/2.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/2.0.0 http://maven.apache.org/xsd/assembly-1.1.3.xsd"> + <id>codegraphs</id> + <formats> + <format>zip</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <fileSets> + <fileSet> + <directory>${codegraphs.directory}</directory> + <outputDirectory></outputDirectory> + <includes> + <include>**</include> + </includes> + </fileSet> + </fileSets> +</assembly> \ No newline at end of file
diff --git a/frameworks/scripts/verify-codegraph-package.js b/frameworks/scripts/verify-codegraph-package.js new file mode 100644 index 0000000..20c1eca --- /dev/null +++ b/frameworks/scripts/verify-codegraph-package.js
@@ -0,0 +1,117 @@ +#!/usr/bin/env node + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); + +function parseArguments(args) { + let root = process.cwd(); + for (let index = 0; index < args.length; index++) { + if (args[index] !== "--root" || index + 1 >= args.length) { + throw new Error(`Unknown or incomplete argument: ${args[index]}`); + } + root = args[++index]; + } + return path.resolve(root); +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function listFiles(root, directory = root) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const absolutePath = path.join(directory, entry.name); + return entry.isDirectory() ? listFiles(root, absolutePath) : + [path.relative(root, absolutePath).split(path.sep).join("/")]; + }); +} + +function resolvePackagedPath(root, relativePath) { + const normalizedPath = path.posix.normalize(relativePath); + if (normalizedPath !== relativePath || normalizedPath.startsWith("../") || path.isAbsolute(relativePath)) { + throw new Error(`Invalid packaged path: ${relativePath}`); + } + return path.join(root, ...relativePath.split("/")); +} + +function main() { + const root = parseArguments(process.argv.slice(2)); + const index = readJson(path.join(root, "index.json")); + const mxml = readJson(path.join(root, "mxml.json")); + if (index.schemaVersion !== "1.0" || mxml.schemaVersion !== index.schemaVersion) { + throw new Error("Codegraph index schema versions are missing or inconsistent"); + } + if (!index.version || mxml.version !== index.version || !Array.isArray(index.modules)) { + throw new Error("Codegraph index versions are missing or inconsistent"); + } + if (!mxml.targets || !Array.isArray(mxml.targets.js) || !Array.isArray(mxml.targets.swf)) { + throw new Error("MXML index must contain JS and SWF target mappings"); + } + + const expectedFiles = new Set(["index.json", "mxml.json"]); + const moduleNames = new Set(); + index.modules.forEach(module => { + if (!module.name || moduleNames.has(module.name)) { + throw new Error(`Missing or duplicate module name: ${module.name}`); + } + moduleNames.add(module.name); + ["js", "swf"].forEach(target => { + const targetIndex = module.targets && module.targets[target]; + if (!targetIndex || !targetIndex.path || !targetIndex.sha256) { + throw new Error(`${module.name} is missing its ${target} graph index`); + } + const graphFile = resolvePackagedPath(root, targetIndex.path); + const contents = fs.readFileSync(graphFile); + const digest = crypto.createHash("sha256").update(contents).digest("hex"); + if (digest !== targetIndex.sha256) { + throw new Error(`${targetIndex.path} does not match its SHA-256 index`); + } + const graph = JSON.parse(contents.toString("utf8")); + if (graph.schemaVersion !== index.schemaVersion || graph.module !== module.name || graph.target !== target) { + throw new Error(`${targetIndex.path} has inconsistent graph identity`); + } + if (!Array.isArray(graph.symbols) || graph.symbols.length !== targetIndex.symbolCount) { + throw new Error(`${targetIndex.path} has an inconsistent symbol count`); + } + const classCount = graph.symbols.filter(symbol => symbol.kind === "class").length; + if (classCount !== targetIndex.classCount) { + throw new Error(`${targetIndex.path} has an inconsistent class count`); + } + expectedFiles.add(targetIndex.path); + }); + }); + + const actualFiles = listFiles(root); + const unexpectedFiles = actualFiles.filter(file => !expectedFiles.has(file)); + const missingFiles = Array.from(expectedFiles).filter(file => !actualFiles.includes(file)); + if (unexpectedFiles.length || missingFiles.length) { + throw new Error(`Package membership mismatch; missing: ${missingFiles.join(", ") || "none"}; ` + + `unexpected: ${unexpectedFiles.join(", ") || "none"}`); + } + console.log(`Verified packaged codegraphs for ${index.modules.length} module(s) and ${actualFiles.length} file(s).`); +} + +try { + main(); +} catch (error) { + console.error(error.message); + process.exitCode = 1; +} \ No newline at end of file
diff --git a/npm/README.md b/npm/README.md index 37416dd..682ed3b 100644 --- a/npm/README.md +++ b/npm/README.md
@@ -41,6 +41,10 @@ node publish.js --type=js-swf --pathToTarball=path-to-tgz-file --username=npm-username --password=npm-password ``` +Both release tarballs must contain the generated public API code graphs under +`royale-asjs/frameworks/codegraphs`, including `index.json` and `mxml.json`. +The npm publisher streams the supplied tarball unchanged. + For example: ``` node publish.js --type=js-only --pathToTarball="C:\p\os\flexroot\royale\royale-asjs\out\binaries\apache-royale-0.9.0-bin-js.tar.gz" --username=apache-royale-owner --password=shared_in_private