initial crime
This commit is contained in:
commit
ea59080a5d
10 changed files with 505 additions and 0 deletions
68
package-lock.json
generated
Normal file
68
package-lock.json
generated
Normal 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
20
package.json
Normal 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
10
src/cargo.js
Normal 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
149
src/config.js
Normal 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
27
src/crates.js
Normal 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
54
src/http.js
Normal 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
65
src/index.js
Normal 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
44
src/linker.js
Normal 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
68
src/project.js
Normal 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
0
src/util.js
Normal file
Loading…
Reference in a new issue