@clunkyard/cli 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clunkyard/cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Shared local builds, matches and replays for Clunkyard robots",
5
5
  "license": "MIT",
6
6
  "engines": {
package/project.cjs CHANGED
@@ -41,12 +41,11 @@ function makefile() {
41
41
 
42
42
  function readme(language,entry) {
43
43
  const row=catalogue.languages[language],portable=nodeLanguage(language)||row.portable;
44
- const tools=row.tools.filter(tool=>!['node','make'].includes(tool));
45
44
  const guide=language==='typescript'?'javascript':language==='wat'||language.endsWith('-wasm')?'wasm':language;
46
45
  return `# A Clunkyard robot in ${language}
47
46
 
48
- Edit \`${entry}\`, build, play a real local match and inspect its replay. Install Node 20+ with npm${tools.length?' and your existing language tools: '+tools.map(tool=>'\`'+tool+'\`').join(', '):''}.
49
- ${portable?'This language uses the portable Node/WebAssembly route.':'Native SDK assets require Linux x86-64 and glibc 2.39+ (Ubuntu 24.04+ or WSL2 Ubuntu 24.04+).'}
47
+ Edit \`${entry}\`, build, play a real local match and inspect its replay. Install [Node 20+ with npm](https://nodejs.org/en/download) and your language's normal compiler or interpreter.
48
+ ${portable?'This language uses the portable Node/WebAssembly route.':'Check the [supported operating systems and toolchain installers](https://clunkyard.com/sdk/player-kit#supported-platforms) for this language. Hosted builds and trials are available when its local toolchain or SDK is unavailable on your computer.'}
50
49
 
51
50
  \`\`\`sh
52
51
  git clone https://github.com/clunkyard/starter-${language}.git
@@ -72,7 +71,7 @@ npx --yes @clunkyard/cli@2 match 77 3d zero-g
72
71
 
73
72
  Keep a baseline commit. Repeat several seeds and rulesets; compare contacts, battery, aim, shots, hits and eliminations in the replay. Local trials run programs as your user. Use hosted trials to measure arena budgets before a tournament.
74
73
 
75
- The CLI packages SDK ${catalogue.sdkVersion}/protocol 2 asset identities from the immutable [${catalogue.release} release](https://github.com/clunkyard/game-kit/releases/tag/${catalogue.release}). \`@2\` receives compatible tooling fixes without copying scripts into your robot repository. To reproduce an exact tool version, use \`npx --yes @clunkyard/cli@2.0.1\` or set \`CLUNKYARD='npx --yes @clunkyard/cli@2.0.1'\` with Make. SDK/engine caches are identified by their content hashes and remain reusable across CLI updates.
74
+ The CLI packages SDK ${catalogue.sdkVersion}/protocol 2 asset identities from the immutable [${catalogue.release} release](https://github.com/clunkyard/game-kit/releases/tag/${catalogue.release}). \`@2\` receives compatible tooling fixes without copying scripts into your robot repository. To reproduce an exact tool version, use \`npx --yes @clunkyard/cli@2.1.0\` or set \`CLUNKYARD='npx --yes @clunkyard/cli@2.1.0'\` with Make. SDK/engine caches are identified by their content hashes and remain reusable across CLI updates.
76
75
 
77
76
  ## Play online
78
77
 
package/runtime.cjs CHANGED
@@ -6,6 +6,37 @@ const {extract,readAsset}=require('./archive.cjs');
6
6
  const catalogue=require('./catalogue.json');
7
7
  const {readManifest}=require('./project.cjs');
8
8
 
9
+ function selectPlatformSdk(lock,platform=process.platform,arch=process.arch) {
10
+ if(lock.portable)return lock.sdk;
11
+ const key=`${platform}-${arch}`,asset=lock.sdkPlatforms?.[key];
12
+ if(asset)return asset;
13
+ if(key==='linux-x64')return lock.sdk;
14
+ throw new Error(`No native ${lock.language} SDK is released for ${key}. Use a hosted build or trial, or install a matching SDK with CLUNKYARD_SDK. See the language guide for toolchain requirements.`);
15
+ }
16
+
17
+ // Execute the worker's environment-prefixed recipes without requiring Unix env
18
+ // on the player's machine. Arguments stay separate, including paths with spaces.
19
+ function recipeInvocation(command,args,{cwd,env}) {
20
+ if(command!=='env')return {command,args,cwd,env};
21
+ args=[...args];env={...env};
22
+ if(args[0]==='--chdir'){args.shift();cwd=args.shift();}
23
+ while(args.length&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0])){
24
+ const value=args.shift(),index=value.indexOf('=');env[value.slice(0,index)]=value.slice(index+1);
25
+ }
26
+ command=args.shift();
27
+ if(!command)throw new Error('The local recipe has no executable.');
28
+ return {command,args,cwd,env};
29
+ }
30
+
31
+ function bindingCompilerIdentity(lock,environment,probe=spawnSync,platform=process.platform){
32
+ return lock.tools.filter(tool=>!['node','make'].includes(tool)&&!(platform==='win32'&&lock.language==='rust'&&tool==='cc')).map(tool=>{
33
+ const variable={rustc:'RUSTC',cc:'CC','c++':'CXX',swiftc:'SWIFTC'}[tool];
34
+ const command=variable&&environment[variable]||tool;
35
+ const result=probe(command,[tool==='javac'?'-version':'--version'],{env:environment,encoding:'utf8',timeout:10000,maxBuffer:64*1024});
36
+ return {command,status:result.status,version:(String(result.stdout||'')+String(result.stderr||'')).slice(0,4096),error:result.error?.code||null};
37
+ });
38
+ }
39
+
9
40
  function selectLocalResourceMode(bot,supported) {
10
41
  if(supported.includes('open'))return 'open';
11
42
  const metered=supported.find(mode=>mode==='dsl'||mode==='wasm');
@@ -17,24 +48,36 @@ function createRuntime(project,entry=path.join(__dirname,'cli.cjs')) {
17
48
  const root=path.resolve(project),initial=readManifest(root),lock=catalogue.languages[initial.language];
18
49
  const cache=path.join(root,'.clunkyard');
19
50
  const engine=path.join(cache,'engine-'+lock.engine.sha256.slice(0,16));
20
- const sdk=process.env.CLUNKYARD_SDK||path.join(cache,'sdk-'+lock.sdk.sha256.slice(0,16));
21
51
  const npmSDK=['javascript','typescript'].includes(initial.language)?catalogue.npmSDK:null;
52
+ const suppliedSdk=process.env.CLUNKYARD_SDK?path.resolve(process.env.CLUNKYARD_SDK):null;
53
+ let sdkAsset,platformError,suppliedMetadata;
54
+ try{sdkAsset=npmSDK?lock.sdk:selectPlatformSdk(lock);}catch(error){platformError=error;}
55
+ if(suppliedSdk&&fs.existsSync(path.join(suppliedSdk,'platform.json')))suppliedMetadata=JSON.parse(fs.readFileSync(path.join(suppliedSdk,'platform.json'),'utf8'));
56
+ const sdk=suppliedSdk||path.join(cache,'sdk-'+(sdkAsset?.sha256.slice(0,16)||'unavailable'));
57
+ const library=suppliedMetadata?.library||sdkAsset?.library||({darwin:'libclunkyard.2.dylib',win32:'clunkyard.dll'}[process.platform]||'libclunkyard.so.2');
58
+ if(path.basename(library)!==library||library.includes('\\'))throw new Error('SDK library must be a file name.');
22
59
  const manifest=()=>readManifest(root);
23
60
  const sha=data=>crypto.createHash('sha256').update(data).digest('hex');
24
61
  const skip=new Set(['.git','.clunkyard','build','replays','node_modules','__pycache__','obj','bin']);
25
62
 
26
63
  function run(command, args, options={}) {
27
- const result=spawnSync(command,args,{cwd:root,env:environment(),stdio:'inherit',...options});
64
+ const invocation=recipeInvocation(command,args,{cwd:options.cwd||root,env:options.env||environment()});
65
+ command=invocation.command;
66
+ if(process.platform==='win32'&&command==='python3')command=process.env.PYTHON||'python';
67
+ const result=spawnSync(command,invocation.args,{stdio:'inherit',...options,cwd:invocation.cwd,env:invocation.env});
28
68
  if(result.error)throw new Error(`${command}: ${result.error.message}. See README.md for this language's toolchain.`);
29
69
  if(result.status!==0)throw new Error(`${command} exited with status ${result.status}`);
30
70
  return result;
31
71
  }
32
72
  function environment() {
33
73
  const paths=[path.join(sdk,'lib'),process.env.LD_LIBRARY_PATH].filter(Boolean).join(path.delimiter);
34
- return {...process.env,CLUNKYARD_SDK:sdk,CLUNKYARD_PROJECT:root,CLUNKYARD_LIB:path.join(sdk,'lib','libclunkyard.so.2'),LD_LIBRARY_PATH:paths,
74
+ return {...process.env,CLUNKYARD_SDK:sdk,CLUNKYARD_PROJECT:root,CLUNKYARD_LIB:path.join(sdk,'lib',library),LD_LIBRARY_PATH:paths,
75
+ ...(process.platform==='darwin'?{DYLD_LIBRARY_PATH:[path.join(sdk,'lib'),process.env.DYLD_LIBRARY_PATH].filter(Boolean).join(path.delimiter)}:{}),
76
+ ...(process.platform==='win32'?{PATH:[path.join(sdk,'lib'),process.env.PATH].filter(Boolean).join(path.delimiter),CC:cCompiler(false),CXX:cCompiler(true)}:{}),
35
77
  TMPDIR:path.join(cache,'tmp'),NODE_PATH:npmSDK?path.join(root,'node_modules'):path.join(sdk,'bindings','javascript'),
36
78
  DOTNET_CLI_TELEMETRY_OPTOUT:'1',DOTNET_NOLOGO:'1'};
37
79
  }
80
+ function cCompiler(cpp){return process.env[cpp?'CXX':'CC']||(process.platform==='win32'?(toolAvailable(cpp?'clang++':'clang')?(cpp?'clang++':'clang'):'cl'):(cpp?'c++':'cc'));}
38
81
  function safeCache() {
39
82
  if(fs.existsSync(cache)&&(!fs.lstatSync(cache).isDirectory()||fs.lstatSync(cache).isSymbolicLink()))throw new Error('.clunkyard must be an ordinary directory');
40
83
  fs.mkdirSync(cache,{recursive:true});fs.mkdirSync(path.join(cache,'tmp'),{recursive:true});
@@ -50,12 +93,21 @@ async function download(asset, destination) {
50
93
  }
51
94
  async function setup() {
52
95
  safeCache();
53
- if(!npmSDK&&!lock.portable&&(process.platform!=='linux'||process.arch!=='x64'))throw new Error('This native SDK release supports Linux x86-64 (Ubuntu 24.04+/WSL2). Use JavaScript/Vane/Cadence/Glyph for the portable Node route; other native platforms are not yet qualified.');
96
+ checkNativePlatform();
54
97
  await download(lock.engine,engine);
55
98
  if(npmSDK)installNpmSDK();
56
- else if(!process.env.CLUNKYARD_SDK)await download(lock.sdk,sdk);
99
+ else if(!suppliedSdk)await download(sdkAsset,sdk);
57
100
  else if(fs.readFileSync(path.join(sdk,'VERSION'),'utf8').trim()!==lock.sdkVersion)throw new Error('CLUNKYARD_SDK points to another SDK version');
58
101
  }
102
+ function checkNativePlatform(){
103
+ if(npmSDK||lock.portable)return;
104
+ if(suppliedSdk){
105
+ if(suppliedMetadata&&suppliedMetadata.platform!==`${process.platform}-${process.arch}`)throw new Error(`CLUNKYARD_SDK targets ${suppliedMetadata.platform}, not ${process.platform}-${process.arch}.`);
106
+ if(!suppliedMetadata&&process.platform!=='linux')throw new Error('A custom native SDK on this platform needs platform.json from the native SDK builder.');
107
+ }else if(platformError)throw platformError;
108
+ const glibc=process.report?.getReport().header.glibcVersionRuntime;
109
+ if(process.platform==='linux'&&(!glibc||Number(glibc.split('.')[0])<2||(Number(glibc.split('.')[0])===2&&Number(glibc.split('.')[1])<39)))throw new Error('This native Linux SDK needs glibc 2.39+ (Ubuntu 24.04+ or WSL2). Use a hosted build or trial on other systems.');
110
+ }
59
111
  function installNpmSDK() {
60
112
  const packageFile=path.join(root,'package.json'),lockFile=path.join(root,'package-lock.json');
61
113
  if(!fs.existsSync(packageFile)||!fs.existsSync(lockFile))throw new Error('JavaScript/TypeScript projects need the npm SDK dependency and package-lock.json. Run clunkyard migrate for an older generated starter, or follow the SDK npm guide.');
@@ -94,7 +146,9 @@ function rebuildBinding() {
94
146
  // This downloads no compiler and keeps native modules compatible locally.
95
147
  for(const binding of npmSDK?[]:(lock.rebuildBindings||[])){
96
148
  const directory=path.join(sdk,'binding-src',binding);
97
- run('make',['-C',directory,'install',`SDK=${sdk}`,`ROOT=${engine}`,`BUILD=${path.join(cache,'tmp','binding-'+binding)}`]);
149
+ const installer=path.join(directory,'install.cjs');
150
+ if(fs.existsSync(installer))run(process.execPath,[installer,sdk]);
151
+ else run('make',['-C',directory,'install',`SDK=${sdk}`,`ROOT=${engine}`,`BUILD=${path.join(cache,'tmp','binding-'+binding)}`]);
98
152
  }
99
153
  }
100
154
  function typescriptCompiler() {
@@ -115,11 +169,11 @@ function typescriptCompiler() {
115
169
  }
116
170
  async function build() {
117
171
  await setup();const m=manifest(),out=path.join(cache,'build');fs.mkdirSync(out,{recursive:true});
118
- const bindingStamp=path.join(cache,'bindings-ready'),bindingIdentity=npmSDK?.integrity||lock.sdk.sha256;
172
+ const bindingStamp=path.join(cache,'bindings-ready'),bindingIdentity=npmSDK?.integrity||sha(Buffer.from(JSON.stringify({asset:suppliedSdk?{sdk,metadata:suppliedMetadata||{},version:lock.sdkVersion}:sdkAsset.sha256,compilers:lock.rebuildBindings?.length?bindingCompilerIdentity(lock,environment()):[]})));
119
173
  if(!fs.existsSync(bindingStamp)||fs.readFileSync(bindingStamp,'utf8')!==bindingIdentity){rebuildBinding();fs.writeFileSync(bindingStamp,bindingIdentity);}
120
174
  const copied=path.join(cache,'source');fs.rmSync(copied,{recursive:true,force:true});sources(root,copied);
121
175
  const action=npmSDK&&m.language==='typescript'?[...typescriptCompiler(),'--ignoreConfig','--ignoreDeprecations','6.0','--target','ES2022','--module','commonjs','--rootDir',copied,'--outDir',out,path.join(copied,m.entry)]:worker(copied).command(m.language,path.join(copied,m.entry),out,m);
122
- if(action){const steps=Array.isArray(action[0])?action:[action];for(const step of steps){const args=step.map(rewrite);if(m.language==='c')args[0]=process.env.CC||'cc';if(m.language==='cpp')args[0]=process.env.CXX||'c++';run(args[0],args.slice(1),{cwd:copied});}}
176
+ if(action){const steps=Array.isArray(action[0])?action:[action];for(const step of steps){const args=step.map(rewrite);if(m.language==='c')args[0]=cCompiler(false);if(m.language==='cpp')args[0]=cCompiler(true);run(args[0],args.slice(1),{cwd:copied});}}
123
177
  const runtimeSource=path.join(out,'source');fs.rmSync(runtimeSource,{recursive:true,force:true});sources(copied,runtimeSource);
124
178
  if(m.language==='typescript')fs.writeFileSync(path.join(out,'package.json'),'{"type":"commonjs"}\n');
125
179
  fs.writeFileSync(path.join(cache,'local-bot.json'),JSON.stringify({...m,command:[process.execPath,entry,'--project',root,'stdio']},null,2)+'\n');
@@ -184,12 +238,13 @@ function toolAvailable(tool){
184
238
  function doctor(){
185
239
  console.log(`Clunkyard SDK ${lock.sdkVersion}; ${lock.language}; ${process.platform}/${process.arch}; Node ${process.versions.node}`);
186
240
  let missing=0;
187
- for(const tool of lock.tools.filter(tool=>tool!=='make'||(!npmSDK&&lock.rebuildBindings?.length))){
188
- const ok=toolAvailable(tool);
189
- console.log(`${ok?'found':'missing'} ${tool}`);if(!ok)missing++;
241
+ for(const tool of lock.tools.filter(tool=>!(process.platform==='win32'&&lock.language==='rust'&&tool==='cc')).filter(tool=>tool!=='make'||(!npmSDK&&lock.rebuildBindings?.some(binding=>!sdkAsset?.portableBindingInstallers?.includes(binding)&&!fs.existsSync(path.join(sdk,'binding-src',binding,'install.cjs')))))){
242
+ const command=process.platform==='win32'?({python3:process.env.PYTHON||'python',cc:cCompiler(false),'c++':cCompiler(true)}[tool]||tool):tool;
243
+ const ok=toolAvailable(command);
244
+ console.log(`${ok?'found':'missing'} ${command}`);if(!ok)missing++;
190
245
  }
191
246
  for(const check of lock.requirements?.checks||[]){
192
- let command=[check.command];
247
+ let command=[process.platform==='win32'&&check.command==='python3'?(process.env.PYTHON||'python'):check.command];
193
248
  if(npmSDK&&check.command==='tsc'){try{command=typescriptCompiler()}catch{}}
194
249
  const result=spawnSync(command[0],[...command.slice(1),...check.args],{encoding:'utf8',env:environment(),timeout:10000});
195
250
  const output=String(result.stdout||'')+String(result.stderr||'');
@@ -200,10 +255,9 @@ function doctor(){
200
255
  if(lock.language==='clojure'){try{console.log('ok Clojure runtime: '+clojureClasspath())}catch(error){console.log('missing '+error.message);missing++}}
201
256
  if(lock.language==='scala'&&process.env.SCALA_LIBRARY_JAR&&!fs.existsSync(process.env.SCALA_LIBRARY_JAR)){console.log('missing SCALA_LIBRARY_JAR file');missing++}
202
257
  if(lock.language==='scala'&&!process.env.SCALA_LIBRARY_JAR&&!fs.existsSync('/usr/share/scala-2.11/lib/scala-library.jar')&&spawnSync('scala',['-version'],{timeout:10000}).error){console.log('missing Scala runtime; set SCALA_LIBRARY_JAR or install the scala launcher.');missing++}
203
- const glibc=process.report?.getReport().header.glibcVersionRuntime;
204
- if(!npmSDK&&!lock.portable&&(process.platform!=='linux'||process.arch!=='x64'||!glibc||Number(glibc.split('.')[0])<2||(Number(glibc.split('.')[0])===2&&Number(glibc.split('.')[1])<39))){console.log('unsupported native platform: use Linux x86-64 with glibc 2.39+ (Ubuntu 24.04+ or WSL2).');missing++}
258
+ try{checkNativePlatform()}catch(error){console.log(error.message);missing++}
205
259
  for(const note of lock.requirements?.notes||[])console.log(note);
206
- console.log(npmSDK||lock.portable?'Portable Node route.':'Native assets: Linux x86-64, glibc 2.39+; Ubuntu 24.04+/WSL2.');
260
+ console.log(npmSDK||lock.portable?'Portable Node route.':`Native SDK: ${process.platform}-${process.arch}${suppliedSdk?' (user-provided)':sdkAsset?.minimumOS?'; minimum OS '+sdkAsset.minimumOS:''}.`);
207
261
  console.log('No toolchains are downloaded automatically. See README.md for official installers.');
208
262
  if(missing)process.exitCode=1;
209
263
  }
@@ -222,4 +276,4 @@ async function main(action='build',args=[]){
222
276
  return {main};
223
277
  }
224
278
 
225
- module.exports={createRuntime,selectLocalResourceMode};
279
+ module.exports={createRuntime,selectLocalResourceMode,selectPlatformSdk,recipeInvocation,bindingCompilerIdentity};