initial crime

This commit is contained in:
Rowan 2024-11-05 01:41:38 -06:00
commit ea59080a5d
10 changed files with 505 additions and 0 deletions

68
package-lock.json generated Normal file
View file

@ -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"
}
}
}
}

20
package.json Normal file
View file

@ -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"
}
}

10
src/cargo.js Normal file
View file

@ -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}`)
})

149
src/config.js Normal file
View file

@ -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
)
})
})

27
src/crates.js Normal file
View file

@ -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)

54
src/http.js Normal file
View file

@ -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()
})

65
src/index.js Normal file
View file

@ -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)

44
src/linker.js Normal file
View file

@ -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
}
}

68
src/project.js Normal file
View file

@ -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' })
}
}

0
src/util.js Normal file
View file