From adba2e32f0bb2b6ab45fe7b231fa01bdc2b9a9a7 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 06:52:36 -0700 Subject: [PATCH] Add repository-owned formatting commands Summary: Add a single `yarn format` / `yarn format-check` contract for Yoga JavaScript and documentation, C-family sources, Kotlin, and the two Python maintenance scripts. Pin clang-format 21.1.2 through a public DotSlash manifest so it matches the current fbsource formatter, and use ktfmt 0.59.0 and Ruff 0.14.0 across internal and public checkouts. Replace the JavaScript workspace clang-format 15 command, which covered only `wasm_bridge.c`, with the repository-wide command. Generated sources and vendored headers remain excluded. allow-large-files Differential Revision: D119662614 --- .eslintrc.cjs | 2 +- javascript/package.json | 5 +- package.json | 13 ++++ scripts/clang-format | 52 ++++++++++++++ scripts/clang-format.js | 93 ++++++++++++++++++++++++ scripts/format-kotlin.js | 88 +++++++++++++++++++++++ scripts/format-python.js | 150 +++++++++++++++++++++++++++++++++++++++ scripts/format-utils.js | 82 +++++++++++++++++++++ yarn.lock | 24 +++---- 9 files changed, 491 insertions(+), 18 deletions(-) create mode 100755 scripts/clang-format create mode 100644 scripts/clang-format.js create mode 100644 scripts/format-kotlin.js create mode 100644 scripts/format-python.js create mode 100644 scripts/format-utils.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 48b960403f..596173299a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -70,7 +70,7 @@ module.exports = { }, // Node { - files: ['**/.*rc.(c){j,t}s', '**/*.config.?(c){j,t}s'], + files: ['**/.*rc.(c){j,t}s', '**/*.config.?(c){j,t}s', 'scripts/**/*.js'], env: { node: true, }, diff --git a/javascript/package.json b/javascript/package.json index 32e77d586b..80854617af 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -24,8 +24,8 @@ "scripts": { "benchmark": "just benchmark --config just.config.cjs", "build": "just build --config just.config.cjs", - "clang-format": "just clang-format --config just.config.cjs", - "clang-format:fix": "just clang-format --fix --config just.config.cjs", + "clang-format": "yarn --cwd .. format-check-cpp", + "clang-format:fix": "yarn --cwd .. format-cpp", "clean": "just clean --config just.config.cjs", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -43,7 +43,6 @@ "@types/which": "^3.0.0", "@yogalayout/cmake-bin": "3.28.0-1", "babel-register-esm": "^1.2.5", - "clang-format": "^1.8.0", "glob": "^10.4.2", "jest": "^29.3.1", "just-scripts": "^2.1.0", diff --git a/package.json b/package.json index b40b23e4b2..fab50fe141 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,16 @@ "version": "0.0.0", "private": true, "scripts": { + "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin && yarn format-python", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin && yarn format-check-python", + "format-check-cpp": "node ./scripts/clang-format.js --check", + "format-check-javascript": "prettier --check \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", + "format-check-kotlin": "node ./scripts/format-kotlin.js --check", + "format-check-python": "node ./scripts/format-python.js --check", + "format-cpp": "node ./scripts/clang-format.js", + "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", + "format-kotlin": "node ./scripts/format-kotlin.js", + "format-python": "node ./scripts/format-python.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "tsc": "yarn workspaces run tsc", @@ -21,6 +31,9 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.1.7", "eslint-plugin-prettier": "^5.5.6", + "fb-dotslash": "0.5.8", + "glob": "^10.4.2", + "ktfmt": "0.59.0", "prettier": "3.9.4", "typescript": "5.0.4" }, diff --git a/scripts/clang-format b/scripts/clang-format new file mode 100755 index 0000000000..c41c09f07f --- /dev/null +++ b/scripts/clang-format @@ -0,0 +1,52 @@ +#!/usr/bin/env dotslash + +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +{ + "name": "clang-format 21.1.2", + "platforms": { + "linux-aarch64": { + "size": 1725525, + "hash": "sha256", + "digest": "f66d2bcf98df1373df6ab4544a2b881e9816985b606e1144e4c77dc8ac87b826", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/c5/83/61fadfa8d62a288d778e0a1ad2f73b01abca64574ee34c5d6d078e0821da/clang_format-21.1.2-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "linux-x86_64": { + "size": 1805060, + "hash": "sha256", + "digest": "6d7caf74fe89154258ddfd63984c98ffe902ef98f013ac517178fc44d72861ff", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "macos-aarch64": { + "size": 1458874, + "hash": "sha256", + "digest": "fc034652dee24583633177d800bc9deebcc9c65eb7ab53b25bbd0fbd443392a9", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/ba/90/b8230efcff90a8543da3fb7fc09d7077afebaba019eceb1686d4db94cac3/clang_format-21.1.2-py2.py3-none-macosx_11_0_arm64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "macos-x86_64": { + "size": 1440163, + "hash": "sha256", + "digest": "00498efb43d60d7ac4195362009a79936d26145a9a90cdfa7a6013a62ab3c40c", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/7d/97/0bb5a6866dfb5f55f7e6ca79466cb0b0081fccbc9f57887949ff23b5c38a/clang_format-21.1.2-py2.py3-none-macosx_10_9_x86_64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "windows-x86_64": { + "size": 1426244, + "hash": "sha256", + "digest": "c98e195a50c0fa40bb058449511b1b681ca7ad553579aa32425f0cfeca8d81ce", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/fe/0d/3b9c6a41a9eed2d45431d91c0e8608da315cd44d0c24c517bfb686db4b6b/clang_format-21.1.2-py2.py3-none-win_amd64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format.exe" + } + } +} diff --git a/scripts/clang-format.js b/scripts/clang-format.js new file mode 100644 index 0000000000..2922963032 --- /dev/null +++ b/scripts/clang-format.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + * @format + */ + +'use strict'; + +const {findMetaTool, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const OSS_CLANG_FORMAT_DOTSLASH = path.join(__dirname, 'clang-format'); +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MAX_HEADER_BYTES = 4096; +const MAX_FILES_PER_PROCESS = 30; +const SOURCE_GLOB = '**/*.{c,cc,cpp,cu,cuh,cxx,h,hh,hpp,hxx,m,mm,proto,tcc}'; +const IGNORE = [ + '**/.cxx/**', + '**/build/**', + '**/node_modules/**', + '**/Pods/**', + 'lib/**', +]; + +function findClangFormat() { + if (process.env.CLANG_FORMAT != null && process.env.CLANG_FORMAT !== '') { + return {command: process.env.CLANG_FORMAT, prefixArguments: []}; + } + return ( + findMetaTool('tools', 'third-party', 'clang-format', 'clang-format') ?? { + command: require('fb-dotslash'), + prefixArguments: [OSS_CLANG_FORMAT_DOTSLASH], + } + ); +} + +function isGenerated(file) { + let fd; + try { + fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + const header = Buffer.alloc(MAX_HEADER_BYTES); + const bytesRead = fs.readSync(fd, header, 0, header.length, 0); + return header.subarray(0, bytesRead).includes(GENERATED_MARKER); + } catch (error) { + console.warn(`Unable to inspect ${file}: ${String(error)}`); + return false; + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function main() { + const check = process.argv.includes('--check'); + const files = globSync(SOURCE_GLOB, { + cwd: REPO_ROOT, + ignore: IGNORE, + }).filter(file => !isGenerated(file)); + const clangFormat = findClangFormat(); + let exitStatus = 0; + + for (let index = 0; index < files.length; index += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + clangFormat.command, + [ + ...clangFormat.prefixArguments, + ...(check ? ['--dry-run', '--Werror'] : ['-i']), + ...files.slice(index, index + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`clang-format was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main(); diff --git a/scripts/format-kotlin.js b/scripts/format-kotlin.js new file mode 100644 index 0000000000..dace0d87c7 --- /dev/null +++ b/scripts/format-kotlin.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + * @format + */ + +'use strict'; + +const {findJava, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MAX_HEADER_BYTES = 4096; +const MAX_FILES_PER_PROCESS = 100; + +function isGenerated(file) { + let fd; + try { + fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + const header = Buffer.alloc(MAX_HEADER_BYTES); + const bytesRead = fs.readSync(fd, header, 0, header.length, 0); + return header.subarray(0, bytesRead).includes(GENERATED_MARKER); + } catch (error) { + console.warn(`Unable to inspect ${file}: ${String(error)}`); + return false; + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function main() { + const check = process.argv.includes('--check'); + const ktfmt = + process.env.KTFMT != null && process.env.KTFMT !== '' + ? {command: process.env.KTFMT, prefixArguments: []} + : (() => { + const java = findJava(17); + if (java == null) { + throw new Error( + 'Kotlin formatting requires Java 17 or newer. Install a JDK or set JAVA=/path/to/java.', + ); + } + const jar = + process.env.KTFMT_JAR != null && process.env.KTFMT_JAR !== '' + ? process.env.KTFMT_JAR + : require.resolve('ktfmt/lib/ktfmt.jar'); + return {command: java, prefixArguments: ['-jar', jar]}; + })(); + const files = globSync('**/*.{kt,kts}', { + cwd: REPO_ROOT, + ignore: ['**/.gradle/**', '**/build/**', '**/node_modules/**'], + }).filter(file => !isGenerated(file)); + let exitStatus = 0; + + for (let index = 0; index < files.length; index += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + ktfmt.command, + [ + ...ktfmt.prefixArguments, + '--do-not-remove-unused-imports', + ...(check ? ['--dry-run', '--set-exit-if-changed'] : []), + ...files.slice(index, index + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`ktfmt was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main(); diff --git a/scripts/format-python.js b/scripts/format-python.js new file mode 100644 index 0000000000..0a83e3c917 --- /dev/null +++ b/scripts/format-python.js @@ -0,0 +1,150 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + * @format + */ + +'use strict'; + +const {findMetaTool, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const RUFF_VERSION = '0.14.0'; +const RUFF_ROOT = path.join( + REPO_ROOT, + 'node_modules', + '.cache', + 'yoga-format', + `ruff-${RUFF_VERSION}`, +); +const PYTHON_GLOB = '**/*.py'; +const IGNORE = ['**/build/**', '**/node_modules/**']; + +function run(command, arguments_, options = {}) { + const result = spawnSync(command, arguments_, { + cwd: REPO_ROOT, + stdio: options.quiet === true ? 'ignore' : 'inherit', + ...options, + env: {...(options.env ?? process.env), PWD: REPO_ROOT}, + }); + if (result.error != null) { + if ( + options.allowMissingCommand === true && + result.error.code === 'ENOENT' + ) { + return 1; + } + throw result.error; + } + if (result.signal != null) { + throw new Error(`${command} was terminated by ${result.signal}`); + } + return result.status ?? 1; +} + +function findPython() { + const candidates = + process.platform === 'win32' + ? [ + ['py', ['-3']], + ['python', []], + ] + : [ + ['python3', []], + ['python', []], + ]; + return ( + candidates.find( + ([command, prefixArguments]) => + run( + command, + [ + ...prefixArguments, + '-c', + 'import sys; raise SystemExit(sys.version_info.major != 3)', + ], + {allowMissingCommand: true, quiet: true}, + ) === 0, + ) ?? null + ); +} + +function runRuff(command, prefixArguments, check, environment) { + const files = globSync(PYTHON_GLOB, {cwd: REPO_ROOT, ignore: IGNORE}); + return run( + command, + [...prefixArguments, 'format', ...(check ? ['--check'] : []), ...files], + {env: environment}, + ); +} + +function main() { + const check = process.argv.includes('--check'); + if (process.env.RUFF != null && process.env.RUFF !== '') { + process.exitCode = runRuff(process.env.RUFF, [], check, process.env); + return; + } + + const metaRuff = findMetaTool('tools', 'third-party', 'ruff', 'ruff'); + if (metaRuff != null) { + process.exitCode = runRuff( + metaRuff.command, + metaRuff.prefixArguments, + check, + process.env, + ); + return; + } + + const python = findPython(); + if (python == null) { + throw new Error('Python formatting requires Python 3 with pip.'); + } + const [command, prefixArguments] = python; + const pythonPath = [RUFF_ROOT, process.env.PYTHONPATH] + .filter(Boolean) + .join(path.delimiter); + const environment = {...process.env, PYTHONPATH: pythonPath}; + if ( + run( + command, + [ + ...prefixArguments, + '-c', + `from importlib.metadata import version; raise SystemExit(version('ruff') != '${RUFF_VERSION}')`, + ], + {env: environment, quiet: true}, + ) !== 0 + ) { + fs.mkdirSync(RUFF_ROOT, {recursive: true}); + const installStatus = run(command, [ + ...prefixArguments, + '-m', + 'pip', + 'install', + '--disable-pip-version-check', + '--only-binary=:all:', + `--target=${RUFF_ROOT}`, + `ruff==${RUFF_VERSION}`, + ]); + if (installStatus !== 0) { + process.exitCode = installStatus; + return; + } + } + process.exitCode = runRuff( + command, + [...prefixArguments, '-m', 'ruff'], + check, + environment, + ); +} + +main(); diff --git a/scripts/format-utils.js b/scripts/format-utils.js new file mode 100644 index 0000000000..1140dc01e2 --- /dev/null +++ b/scripts/format-utils.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + * @format + */ + +'use strict'; + +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const FBSOURCE_ROOT = path.resolve(REPO_ROOT, '../..'); + +function findMetaTool(...relativePath) { + const manifest = path.join(FBSOURCE_ROOT, ...relativePath); + const dotslash = ['/usr/bin/dotslash', '/usr/local/bin/dotslash'].find( + candidate => fs.existsSync(candidate), + ); + return dotslash != null && fs.existsSync(manifest) + ? {command: dotslash, prefixArguments: [manifest]} + : null; +} + +function javaMajorVersion(command) { + const result = spawnSync(command, ['-version'], {encoding: 'utf8'}); + const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`; + const version = /version "(?:1\.)?(\d+)/.exec(output); + return result.status === 0 && version != null ? Number(version[1]) : null; +} + +function findJava(minimumVersion) { + if (process.env.JAVA != null && process.env.JAVA !== '') { + const version = javaMajorVersion(process.env.JAVA); + if (version == null) { + throw new Error( + `JAVA=${process.env.JAVA} did not run or report a recognizable Java version.`, + ); + } + if (version < minimumVersion) { + throw new Error( + `JAVA=${process.env.JAVA} is Java ${version}; Java ${minimumVersion} or newer is required.`, + ); + } + return process.env.JAVA; + } + + const candidates = ['java']; + if (process.platform === 'darwin') { + for (const version of [17, 21]) { + const result = spawnSync( + '/usr/libexec/java_home', + ['-v', String(version)], + { + encoding: 'utf8', + }, + ); + if (result.status === 0 && result.stdout.trim() !== '') { + candidates.unshift(path.join(result.stdout.trim(), 'bin', 'java')); + } + } + } else { + candidates.unshift( + '/usr/local/java-runtime/impl/17/bin/java', + '/usr/local/java-runtime/17/bin/java', + '/usr/local/java-runtime/impl/21/bin/java', + '/usr/local/java-runtime/21/bin/java', + ); + } + + return ( + candidates.find(command => javaMajorVersion(command) >= minimumVersion) ?? + null + ); +} + +module.exports = {findJava, findMetaTool, REPO_ROOT}; diff --git a/yarn.lock b/yarn.lock index 2100b5d43a..39a17ac335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3455,11 +3455,6 @@ async-settle@^2.0.0: dependencies: async-done "^2.0.0" -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -3943,15 +3938,6 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== -clang-format@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.8.0.tgz#7779df1c5ce1bc8aac1b0b02b4e479191ef21d46" - integrity sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw== - dependencies: - async "^3.2.3" - glob "^7.0.0" - resolve "^1.1.6" - clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -5394,6 +5380,11 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +fb-dotslash@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5" + integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA== + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -7295,6 +7286,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ktfmt@0.59.0: + version "0.59.0" + resolved "https://registry.yarnpkg.com/ktfmt/-/ktfmt-0.59.0.tgz#99f98b81dbdc7f1487dfbc9850eb17b3780cf6d5" + integrity sha512-lOEn/7y2Ez2/nxDTn5EwJv6BSugB8BtzY2Gn6GvyLIAjdUf3xgKzirIxD57t/vu5I6eybivmVtONI3WGXyZ3lw== + last-run@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/last-run/-/last-run-2.0.0.tgz#f82dcfbfce6e63d041bd83d64c82e34cdba6572e"