commit ea59080a5dbfadcde0d929cfeb6e6acefa9c1e84 Author: kitsunecafe Date: Tue Nov 5 01:41:38 2024 -0600 initial crime diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..49a0a69 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,68 @@ +{ + "name": "bevy-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bevy-cli", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "mini-hamt": "^1.0.3", + "minimist": "^1.2.8", + "rambda": "^9.3.0", + "smol-toml": "^1.3.0" + } + }, + "node_modules/@f/hash-str": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@f/hash-str/-/hash-str-1.0.0.tgz", + "integrity": "sha512-qVQ416Lug2QuN3OW39+4CCDybWVABfAvyydlr9n1xwkE/U1XKB7BC2th1EH02f7P+zr5foycsNisppRKYf+L2Q==", + "license": "MIT" + }, + "node_modules/@f/popcount": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@f/popcount/-/popcount-1.0.0.tgz", + "integrity": "sha512-o6ctcKVEfiVLF2cPSdhR815dpVW4d3KijBu20jcxTbF6NFTV2sO/2+ffFn6NMgiVv3gv/4mVtOF92MYGtrfmew==", + "license": "MIT" + }, + "node_modules/mini-hamt": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mini-hamt/-/mini-hamt-1.0.3.tgz", + "integrity": "sha512-kL1xAjkZl4tWZ2QYNBMbpvmt0QJRIOoMxmdnXtJyPn/6jrBr/TKRbyJRNTsrjcGxlaudR2+CmLPlko7B5IdZJA==", + "license": "MIT", + "dependencies": { + "@f/hash-str": "^1.0.0", + "@f/popcount": "^1.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rambda": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-9.3.0.tgz", + "integrity": "sha512-cl/7DCCKNxmsbc0dXZTJTY08rvDdzLhVfE6kPBson1fWzDapLzv0RKSzjpmAqP53fkQqAvq05gpUVHTrUNsuxg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.0.tgz", + "integrity": "sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1b9455 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "bevy-cli", + "version": "1.0.0", + "main": "src/index.js", + "type": "module", + "scripts": { + "start": "node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "mini-hamt": "^1.0.3", + "minimist": "^1.2.8", + "rambda": "^9.3.0", + "smol-toml": "^1.3.0" + } +} diff --git a/src/cargo.js b/src/cargo.js new file mode 100644 index 0000000..b3911f3 --- /dev/null +++ b/src/cargo.js @@ -0,0 +1,10 @@ +import { promisify } from 'node:util' +import { exec as _exec } from 'node:child_process' + +const exec = promisify(_exec) + +export const Cargo = Object.freeze({ + init: path => exec(`cargo new ${path}`), + add: lib => exec(`cargo add ${lib}`) +}) + diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..68112d1 --- /dev/null +++ b/src/config.js @@ -0,0 +1,149 @@ +import npath from 'node:path' +import { assoc, concat, curry, curryN, lens, merge, pipe, prop, set, view } from 'rambda' +import { map, get as getMap } from './project.js' +import { get as getLinker } from './linker.js' +import { Cargo } from './cargo.js' +import { Crate } from './crates.js' + +const optLevel = value => ({ 'opt-level': value }) + +//const assoc = curry((key, value, obj) => { +// const l = lens(prop(key), assoc(key)) +// return set(l, value, obj) +//}) + +export const Bevy = Object.freeze({ + active: () => true, + apply: curry(({ 'bevy-version': bevy }, project) => + map( + 'Cargo.toml', + assoc('dependencies', { bevy }), + project + )) +}) + +export const DynamicLinking = Object.freeze({ + active: ({ dynlink }) => dynlink, + apply: curry(({ 'bevy-version': version }, project) => + map( + 'Cargo.toml', + assoc('dev-dependencies', { bevy: { version, features: ['dynamic_linking'] }}), + project + )) +}) + +export const DevelopmentOptimizations = Object.freeze({ + active: ({ dynopts }) => dynopts, + apply: curry((_params, project) => + map( + 'Cargo.toml', + pipe( + assoc('profile.dev', optLevel(1)), + assoc('profile.dev.package."*"', optLevel(3)) + ) + )) +}) + +export const ReleaseOptimizations = Object.freeze({ + active: ({ relopts }) => relopts, + apply: curry((_params, project) => { + const relOpts = { 'codegen-units': 1, lto: 'thin' } + const wasmOpts = { inherits: 'release', 'opt-level': 's', strip: 'debuginfo' } + + return map( + 'Cargo.toml', + pipe( + assoc('profile.release', relOpts), + assoc('profile.wasm-release', wasmOpts) + ), + project + ) + }) +}) + +export const CustomLinker = Object.freeze({ + active: ({ linker }) => linker, + apply: curry(({ linker }, project) => { + const linkinfo = getLinker(linker, project.host) + + if (!linkinfo) { + return project + } + + const { target, linker: bin, rustflags: rf1 = [] } = linkinfo + const config = '.cargo/config.toml' + const key = `target.${project.host}` + const { rustflags: rf2 = [], ...rest } = getMap(config, project)[key] || {} + + return map( + config, + assoc(key, { + ...rest, + linker: bin, + rustflags: concat(rf1, rf2) + }), + project + ) + }) +}) + +export const NightlyCompiler = Object.freeze({ + active: ({ nightly }) => nightly, + apply: curry((_params, project) => + map( + 'rust-toolchain.toml', + assoc('toolchain', { channel: 'nightly' }), + project + )) +}) + +export const Cranelift = Object.freeze({ + active: ({ cranelift, nightly }) => nightly && cranelift, + apply: curry((_params, project) => + map( + '.cargo/config.toml', + pipe( + assoc('unstable', { 'codegen-backend': true }), + assoc('profile.dev', { 'codegen-backend': 'cranelift' }), + assoc('profile.dev.package."*"', { 'codegen-backend': 'llvm' }), + ), + project + )) +}) + +export const GenericSharing = Object.freeze({ + active: ({ genshare, nightly }) => nightly && genshare, + apply: curry((_params, project) => { + const config = '.cargo/config.toml' + const key = `target.${project.host}` + const { rustflags = [], ...rest } = getMap(config, project)[key] || {} + + return map( + config, + assoc(key, { + ...rest, + rustflags: [...rustflags, '-Zshare-generics=y'], + }), + project + ) + }) +}) + + +export const ParallelFrontend = Object.freeze({ + active: ({ parallel, nightly }) => nightly && parallel, + apply: curry((_params, project) => { + const config = '.cargo/config.toml' + const key = `target.${project.host}` + const { rustflags = [], ...rest } = getMap(config, project)[key] || {} + + return map( + config, + assoc(key, { + ...rest, + rustflags: [...rustflags, '-Zthreads=0'], + }), + project + ) + }) +}) diff --git a/src/crates.js b/src/crates.js new file mode 100644 index 0000000..e3590fa --- /dev/null +++ b/src/crates.js @@ -0,0 +1,27 @@ +import npath from 'node:path' +import { request } from './http.js' +import { concat, identity, ifElse, isNotEmpty, join, map, pipe, toPairs } from 'rambda' + +const UserAgent = { 'User-Agent': 'bevy-cli' } + +export const RootURL = 'https://crates.io/api/v1' + +const makeParams = ifElse( + isNotEmpty, + pipe( + toPairs, + map(join('=')), + join('&'), + concat('?') + ), + () => identity('') +) + +const get = (path, params = {}) => { + const url = [RootURL, path].join('/') + makeParams(params) + return request(url, { headers: UserAgent }) +} + +export const Crate = (crate, params) => get(npath.join('crates', crate), params) +export const CrateVersions = (crate, params) => get(npath.join('crates', crate, 'versions'), params) + diff --git a/src/http.js b/src/http.js new file mode 100644 index 0000000..8733c92 --- /dev/null +++ b/src/http.js @@ -0,0 +1,54 @@ +import { URL, urlToHttpOptions } from 'node:url' +import https from 'node:https' + +export const Method = Object.freeze({ + GET: 'GET', + HEAD: 'HEAD', + POST: 'POST', + PUT: 'PUT', + DELETE: 'DELETE', + CONNECT: 'CONNECT', + OPTIONS: 'OPTIONS', + TRACE: 'TRACE', + PATCH: 'PATCH' +}) + +const Response = data => Object.freeze({ + buffer: () => Promise.resolve(data), + text: () => new Promise((resolve, reject) => { + try { + resolve(data.toString()) + } catch(e) { + reject(e) + } + }), + json: () => new Promise((resolve, reject) => { + try { + resolve(JSON.parse(data)) + } catch(e) { + reject(e) + } + }) +}) + +export const request = (path, options) => new Promise((resolve, reject) => { + const url = new URL(path) + + const opts = { + ...urlToHttpOptions(url), + ...options + } + + const req = https.request(opts, res => { + let data = [] + + res.on('data', d => { + data.push(d) + }) + + res.on('end', () => resolve(Response(Buffer.concat(data)))) + }) + req.on('error', reject) + req.end() +}) + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..79680ac --- /dev/null +++ b/src/index.js @@ -0,0 +1,65 @@ +import pkg from '../package.json' with { type: 'json' } +import parseArgs from 'minimist' +import path from 'node:path' +import { parse, stringify } from 'smol-toml' +import { reduce } from 'rambda' +import * as Project from './project.js' +import { Cargo } from './cargo.js' +import * as Configurations from './config.js' +import { Crate } from './crates.js' + +const help = () => `Usage: + ${pkg.name} PATH [options] + + Options: + -b, --bevy-version the bevy version to install, default to latest stable + -l, --linker configured linker. valid options: lld, mold (linux only) + -q, --devopt development optimizations + -r, --relopt release optimizations + -d, --dynlink enable dynamic linking + -n, --nightly enable nightly rust compiler + -c, --cranelift enable cranelift codegen (requires nightly) + -g, --genshare enable generic sharing (requires nightly) + -p, --parallel enable rustc's parallel frontend (requires nightly) + + -h, --help print usage information and exit + -v, --version show version info and exit +` + +const applicableConfigs = params => + Object.values(Configurations).filter(c => c.active(params)) + +const version = () => `${pkg.name} v${pkg.version}` + +const main = async argv => { + if(argv.help) { + console.log(help()) + } else if (argv.version) { + console.log(version()) + } else { + const [relativeRoot] = argv._ + const root = path.join(process.cwd(), relativeRoot) + await Cargo.init(root) + const project = Project.create(path.normalize(root)) + const configs = applicableConfigs(argv) + const result = await reduce(async (acc, cfg) => cfg.apply(argv, await acc), project, configs) + await Project.serialize(result) + } +} + +const defaultBevyVersion = () => + Crate('bevy').then(c => c.json()).then(c => c.crate.default_version) + +const defaultLinker = () => process.platform === 'darwin' ? undefined : 'lld' + +const schema = Object.freeze({ + string: ['bevy-version', 'linker'], + boolean: ['devopt', 'relopt', 'dynlink', 'nightly', 'cranelift', 'genshare', 'parallel', 'version'], + alias: { b: 'bevy-version', l: 'linker', g: 'genshare', c: 'cranelift', n: 'nightly', d: 'dynlink', p: 'parallel', v: 'version', h: 'help' }, + default: { 'bevy-version': await defaultBevyVersion(), devopt: true, relopt: true, genshare: true, parallel: true, cranelift: true, nightly: true, dynlink: true, linker: defaultLinker() } +}) + +const argv = parseArgs(process.argv.slice(2), schema) + +main(argv) + diff --git a/src/linker.js b/src/linker.js new file mode 100644 index 0000000..04a043b --- /dev/null +++ b/src/linker.js @@ -0,0 +1,44 @@ +import { path } from 'rambda' + +const Linker = ({ target, linker, rustflags }) => Object.freeze({ target, linker, rustflags }) + +const linuxLld = () => Linker({ + linker: 'clang', + rustflags: ['-C', 'link-arg=-fuse-ld=lld'] +}) + +const macosLld = () => Linker({ + rustflags: [ '-C', 'link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld'] +}) + +const windowsLld = () => Linker({ + linker: 'rust-lld.exe', + rustflags: [] +}) + +const linuxMold = () => Linker({ + linker: 'clang', + rustflags: ['-C', 'link-arg=-fuse-ld=/usr/bin/mold'] + +}) + +export const LinkerType = Object.freeze({ + lld: Object.freeze({ + 'x86_64-pc-windows-msvc': windowsLld, + 'aarch64-apple-darwin': macosLld, + 'x86_64-unknown-linux-gnu': linuxLld + }), + mold: Object.freeze({ + 'x86_64-unknown-linux-gnu': linuxMold + }) +}) + +export const get = (linker, host)=> { + const type = path([linker.toLowerCase(), host], LinkerType) + + if(type) { + return type() + } else { + return undefined + } +} diff --git a/src/project.js b/src/project.js new file mode 100644 index 0000000..896e272 --- /dev/null +++ b/src/project.js @@ -0,0 +1,68 @@ +import fs from 'node:fs/promises' +import npath from 'node:path' +import { parse, stringify } from 'smol-toml' +import { always, cond, curry, equals, flip, partial, pipe, T as True } from 'rambda' +import * as hamt from 'mini-hamt' + +const guessHostTriplet = cond([ + [equals('win32'), always('x86_64-pc-windows-msvc')], + [equals('darwin'), always('aarch64-apple-darwin')], + [True, always('x86_64-unknown-linux-gnu')], + ]) + +export const create = (root, host = guessHostTriplet(process.platform), modified = new Set(), config = hamt.empty) => Object.freeze({ + root, + host, + modified, + config +}) + +const resolve = curry((path, { root }) => { + return npath.isAbsolute(path) ? path : npath.join(root, path) +}) + +export const load = curry((path, project) => { + const absPath = resolve(path, project) + + return fs.readFile(absPath, { encoding: 'utf-8' }) + .then(parse) + .catch(() => ({})) + .then(value => set(path, value, project)) +}) + +export const get = curry((path, { config }) => + hamt.get(config, path) +) + +export const set = curry((path, value, { root, host, modified, config }) => + create( + root, + host, + new Set([path, ...modified]), + hamt.set(config, path, value) + ) +) + +export const map = curry(async (path, fn, project) => { + let p = project + let f = get(path, project) + + if (!f) { + p = await load(path, project) + f = get(path, p) + } + + const r = fn(f) + return set(path, r, p) +}) + +export const serialize = async project => { + for(const path of Array.from(project.modified)) { + const absPath = resolve(path, project) + const dir = npath.dirname(absPath) + await fs.mkdir(dir, { recursive: true }) + const data = Buffer.from(stringify(get(path, project))) + fs.writeFile(absPath, data, { flag: 'w' }) + } +} + diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..e69de29