@@ -39,11 +39,12 @@ async function main(): Promise<void> {
3939 await Promise . all ( manifests . cargoToml . map ( file => updateCargoToml ( file , version ) ) ) ;
4040
4141 const publishableTypeScriptPackages = await getPublishableTypeScriptPackages ( manifests . packageJson ) ;
42+ const publishableRustPackages = await getPublishableRustPackages ( manifests . cargoToml ) ;
4243
4344 await createAndPushCommit ( version ) ;
4445
4546 await publishTypeScriptPackages ( publishableTypeScriptPackages , version ) ;
46- await runCommand ( 'cargo' , [ 'publish' ] , path . join ( repoRoot , 'rust' ) ) ;
47+ await publishRustPackages ( publishableRustPackages , version ) ;
4748}
4849
4950function isValidVersion ( version : string ) : boolean {
@@ -158,14 +159,14 @@ async function updateCargoToml(filePath: string, version: string): Promise<void>
158159 console . log ( `Updated ${ relative ( filePath ) } to ${ version } ` ) ;
159160}
160161
161- type PublishablePackage = {
162+ type PublishableTypeScriptPackage = {
162163 name : string ;
163164 directory : string ;
164165} ;
165166
166- async function getPublishableTypeScriptPackages ( packageJsonPaths : string [ ] ) : Promise < PublishablePackage [ ] > {
167+ async function getPublishableTypeScriptPackages ( packageJsonPaths : string [ ] ) : Promise < PublishableTypeScriptPackage [ ] > {
167168 const tsRoot = path . join ( repoRoot , 'typescript' ) ;
168- const packages : PublishablePackage [ ] = [ ] ;
169+ const packages : PublishableTypeScriptPackage [ ] = [ ] ;
169170
170171 for ( const filePath of packageJsonPaths ) {
171172 const isTypeScriptPackage =
@@ -209,7 +210,7 @@ async function getPublishableTypeScriptPackages(packageJsonPaths: string[]): Pro
209210 return packages ;
210211}
211212
212- async function publishTypeScriptPackages ( packages : PublishablePackage [ ] , version : string ) : Promise < void > {
213+ async function publishTypeScriptPackages ( packages : PublishableTypeScriptPackage [ ] , version : string ) : Promise < void > {
213214 if ( packages . length === 0 ) {
214215 console . warn ( 'No publishable TypeScript packages found, skipping npm publish' ) ;
215216 return ;
@@ -244,6 +245,166 @@ async function packageVersionExists(name: string, version: string): Promise<bool
244245 }
245246}
246247
248+ type PublishableRustPackage = {
249+ name : string ;
250+ directory : string ;
251+ localDependencies : string [ ] ;
252+ } ;
253+
254+ async function getPublishableRustPackages ( cargoTomlPaths : string [ ] ) : Promise < PublishableRustPackage [ ] > {
255+ const rustRoot = path . join ( repoRoot , 'rust' ) ;
256+ const packages : PublishableRustPackage [ ] = [ ] ;
257+
258+ for ( const filePath of cargoTomlPaths ) {
259+ const normalized = path . normalize ( filePath ) ;
260+ const rustWorkspaceRoot = path . join ( rustRoot , 'Cargo.toml' ) ;
261+
262+ if ( normalized === rustWorkspaceRoot ) {
263+ continue ;
264+ }
265+
266+ const isRustCrate = normalized . startsWith ( `${ rustRoot } ${ path . sep } ` ) ;
267+ if ( ! isRustCrate ) {
268+ continue ;
269+ }
270+
271+ const crateDirectory = path . dirname ( normalized ) ;
272+ const relativeToRustRoot = path . relative ( rustRoot , crateDirectory ) ;
273+ if ( relativeToRustRoot . split ( path . sep ) . includes ( 'examples' ) ) {
274+ continue ;
275+ }
276+
277+ const raw = await fs . readFile ( normalized , 'utf8' ) ;
278+ const lines = raw . split ( / \r ? \n / ) ;
279+
280+ let inPackageSection = false ;
281+ let packageName : string | undefined ;
282+ let publishFlag : string | undefined ;
283+
284+ for ( const line of lines ) {
285+ const trimmed = line . trim ( ) ;
286+
287+ if ( trimmed . startsWith ( '[' ) && trimmed . endsWith ( ']' ) ) {
288+ inPackageSection = trimmed === '[package]' ;
289+ continue ;
290+ }
291+
292+ if ( ! inPackageSection ) {
293+ continue ;
294+ }
295+
296+ if ( ! packageName ) {
297+ const nameMatch = trimmed . match ( / ^ n a m e \s * = \s * " ( [ ^ " ] + ) " / ) ;
298+ if ( nameMatch ) {
299+ packageName = nameMatch [ 1 ] ;
300+ continue ;
301+ }
302+ }
303+
304+ if ( ! publishFlag ) {
305+ const publishMatch = trimmed . match ( / ^ p u b l i s h \s * = \s * ( .+ ) $ / ) ;
306+ if ( publishMatch ) {
307+ publishFlag = publishMatch [ 1 ] . trim ( ) ;
308+ }
309+ }
310+ }
311+
312+ if ( publishFlag && publishFlag . startsWith ( 'false' ) ) {
313+ continue ;
314+ }
315+
316+ if ( ! packageName ) {
317+ console . warn ( `Skipping ${ relative ( normalized ) } (missing package name)` ) ;
318+ continue ;
319+ }
320+
321+ const localDependencies : string [ ] = [ ] ;
322+ const dependencyRegex = / ^ ( [ A - Z a - z 0 - 9 _ - ] + ) \s * = \s * { [ ^ } ] * p a t h \s * = \s * " ( [ ^ " ] + ) " [ ^ } ] * } $ / gm;
323+ let match : RegExpExecArray | null ;
324+ while ( ( match = dependencyRegex . exec ( raw ) ) !== null ) {
325+ localDependencies . push ( match [ 1 ] ) ;
326+ }
327+
328+ packages . push ( {
329+ name : packageName ,
330+ directory : crateDirectory ,
331+ localDependencies
332+ } ) ;
333+ }
334+
335+ return packages ;
336+ }
337+
338+ async function publishRustPackages ( packages : PublishableRustPackage [ ] , version : string ) : Promise < void > {
339+ if ( packages . length === 0 ) {
340+ console . warn ( 'No publishable Rust crates found, skipping cargo publish' ) ;
341+ return ;
342+ }
343+
344+ const ordered = orderRustPackages ( packages ) ;
345+
346+ for ( const pkg of ordered ) {
347+ console . log ( `Preparing to publish ${ pkg . name } @${ version } from ${ relative ( pkg . directory ) } ` ) ;
348+
349+ if ( await rustPackageVersionExists ( pkg . name , version ) ) {
350+ console . log ( `Skipping ${ pkg . name } @${ version } (already published)` ) ;
351+ continue ;
352+ }
353+
354+ await runCommand ( 'cargo' , [ 'publish' ] , pkg . directory ) ;
355+ }
356+
357+ console . log ( 'Published Rust crates' ) ;
358+ }
359+
360+ function orderRustPackages ( packages : PublishableRustPackage [ ] ) : PublishableRustPackage [ ] {
361+ const ordered : PublishableRustPackage [ ] = [ ] ;
362+ const packageMap = new Map ( packages . map ( pkg => [ pkg . name , pkg ] ) ) ;
363+ const visited = new Set < string > ( ) ;
364+ const visiting = new Set < string > ( ) ;
365+
366+ const visit = ( pkg : PublishableRustPackage ) : void => {
367+ if ( visited . has ( pkg . name ) ) {
368+ return ;
369+ }
370+
371+ if ( visiting . has ( pkg . name ) ) {
372+ console . warn ( `Detected cyclic dependency while ordering ${ pkg . name } ` ) ;
373+ return ;
374+ }
375+
376+ visiting . add ( pkg . name ) ;
377+
378+ for ( const dep of pkg . localDependencies ) {
379+ const depPkg = packageMap . get ( dep ) ;
380+ if ( depPkg ) {
381+ visit ( depPkg ) ;
382+ }
383+ }
384+
385+ visiting . delete ( pkg . name ) ;
386+ visited . add ( pkg . name ) ;
387+ ordered . push ( pkg ) ;
388+ } ;
389+
390+ for ( const pkg of packages ) {
391+ visit ( pkg ) ;
392+ }
393+
394+ return ordered ;
395+ }
396+
397+ async function rustPackageVersionExists ( name : string , version : string ) : Promise < boolean > {
398+ try {
399+ const output = await captureCommand ( 'cargo' , [ 'search' , name , '--limit' , '1' ] , repoRoot ) ;
400+ return output
401+ . split ( / \r ? \n / )
402+ . some ( line => line . startsWith ( `${ name } = "${ version } "` ) ) ;
403+ } catch {
404+ return false ;
405+ }
406+ }
407+
247408async function runCommand ( command : string , args : string [ ] , cwd : string ) : Promise < void > {
248409 console . log ( `Running ${ command } ${ args . join ( ' ' ) } in ${ relative ( cwd ) } ` ) ;
249410
0 commit comments