@@ -459,7 +459,7 @@ export function notNull(el: any): boolean {
459459 return el !== null ;
460460}
461461
462- /** Determine the compose command to use (`docker-compose` or `docker compose`). */
462+ /** Determine the compose command to use (`docker-compose`, `docker compose`, `podman compose`, or `podman- compose`). */
463463async function composeCommand ( cwd ?: string ) : Promise < string > {
464464 return new Promise < string > ( ( resolve ) => {
465465 let cmd = "docker compose" ;
@@ -468,11 +468,89 @@ async function composeCommand(cwd?: string): Promise<string> {
468468 // 'docker compose' is not present, so default to 'docker-compose'
469469 cmd = "docker-compose" ;
470470 }
471- resolve ( cmd ) ;
471+ exec ( `${ cmd } version` , { cwd } , ( error ) => {
472+ if ( error ) {
473+ // Neither Docker Compose variant is present; try Podman Compose. 'podman compose' can locate a
474+ // provider that a plain PATH lookup for 'podman-compose' wouldn't, so try it first.
475+ cmd = "podman compose" ;
476+ }
477+ exec ( `${ cmd } version` , { cwd } , ( error ) => {
478+ if ( error ) {
479+ cmd = "podman-compose" ;
480+ }
481+ resolve ( cmd ) ;
482+ } ) ;
483+ } ) ;
472484 } ) ;
473485 } ) ;
474486}
475487
488+ /**
489+ * Podman's compose tooling doesn't support the same `ps --services --filter status=running` and
490+ * `port` output format as Docker Compose (bare port number instead of a host:port pair, and no
491+ * `--services`/`--filter` flags), so it needs its own port/running-state resolution.
492+ */
493+ async function portFromPodmanCompose (
494+ cmd : string ,
495+ cwd : string ,
496+ file : string ,
497+ service : string ,
498+ internalPort : number ,
499+ internalSuperserverPort : number ,
500+ dockerCompose : { internalSuperserverPort ?: number } ,
501+ result : { port : number | null ; superserverPort : number | null ; docker : boolean ; service ?: string }
502+ ) : Promise < typeof result > {
503+ const ps = await run ( "ps --format json" ) ;
504+ let containers : { State ?: string ; Labels ?: Record < string , string > } [ ] = [ ] ;
505+ try {
506+ containers = JSON . parse ( ps ) ;
507+ } catch {
508+ // Leave containers empty; treated as not running below
509+ }
510+ if ( ! containers . some ( ( c ) => c . State === "running" && c . Labels ?. [ "com.docker.compose.service" ] === service ) ) {
511+ throw `Service '${ service } ' not found in '${ path . join ( cwd , file ) } ', or not running.` ;
512+ }
513+ const port = parsePort ( await run ( `port --protocol=tcp ${ service } ${ internalPort } ` ) ) ;
514+ if ( ! port ) {
515+ throw `Webserver port ${ internalPort } not published for service '${ service } ' in '${ path . join ( cwd , file ) } '.` ;
516+ }
517+ result . port = parseInt ( port , 10 ) ;
518+ let superserverPort : string | undefined ;
519+ try {
520+ superserverPort = parsePort ( await run ( `port --protocol=tcp ${ service } ${ internalSuperserverPort } ` ) ) ;
521+ } catch ( error ) {
522+ // Not an error if we were merely looking for the default port and the container doesn't publish it
523+ if ( ! dockerCompose . internalSuperserverPort ) return result ;
524+ throw error ;
525+ }
526+ if ( ! superserverPort ) {
527+ throw `Superserver port ${ internalSuperserverPort } not published for service '${ service } ' in '${ path . join ( cwd , file ) } '.` ;
528+ }
529+ result . superserverPort = parseInt ( superserverPort , 10 ) ;
530+ return result ;
531+
532+ // Runs a compose subcommand and returns its stdout, rejecting with a plain string on failure.
533+ function run ( args : string ) : Promise < string > {
534+ return new Promise ( ( resolve , reject ) => {
535+ exec ( `${ cmd } ${ args } ` , { cwd } , ( error , stdout ) => {
536+ if ( error ) {
537+ reject ( error . message ) ;
538+ return ;
539+ }
540+ resolve ( stdout ) ;
541+ } ) ;
542+ } ) ;
543+ }
544+
545+ function parsePort ( stdout : string ) {
546+ return stdout
547+ . trim ( )
548+ . split ( "\n" )
549+ . pop ( )
550+ ?. match ( / ( \d + ) $ / ) ?. [ 1 ] ;
551+ }
552+ }
553+
476554export async function portFromDockerCompose (
477555 workspaceFolderName ?: string
478556) : Promise < { port : number | null ; superserverPort : number | null ; docker : boolean ; service ?: string } > {
@@ -529,6 +607,10 @@ export async function portFromDockerCompose(
529607 const envFileParam = envFile ? `--env-file ${ envFile } ` : "" ;
530608 const cmd = `${ await composeCommand ( cwd ) } -f ${ file } ${ envFileParam } ` ;
531609
610+ if ( cmd . startsWith ( "podman compose " ) || cmd . startsWith ( "podman-compose " ) ) {
611+ return portFromPodmanCompose ( cmd , cwd , file , service , internalPort , internalSuperserverPort , dockerCompose , result ) ;
612+ }
613+
532614 return new Promise ( ( resolve , reject ) => {
533615 exec ( `${ cmd } ps --services --filter status=running` , { cwd } , ( error , stdout ) => {
534616 if ( error ) {
@@ -580,7 +662,7 @@ export async function terminalWithDocker(): Promise<vscode.Terminal> {
580662 if ( ! terminal ) {
581663 let exe = await composeCommand ( ) ;
582664 const argsArr : string [ ] = [ ] ;
583- if ( exe == "docker compose" ) {
665+ if ( exe . includes ( " " ) ) {
584666 const exeSplit = exe . split ( " " ) ;
585667 exe = exeSplit [ 0 ] ;
586668 argsArr . push ( exeSplit [ 1 ] ) ;
@@ -615,7 +697,7 @@ export async function shellWithDocker(): Promise<vscode.Terminal> {
615697 if ( ! terminal ) {
616698 let exe = await composeCommand ( ) ;
617699 const argsArr : string [ ] = [ ] ;
618- if ( exe == "docker compose" ) {
700+ if ( exe . includes ( " " ) ) {
619701 const exeSplit = exe . split ( " " ) ;
620702 exe = exeSplit [ 0 ] ;
621703 argsArr . push ( exeSplit [ 1 ] ) ;
0 commit comments