Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/builder/api/builder.api
Original file line number Diff line number Diff line change
Expand Up @@ -2704,14 +2704,16 @@ public final class elide/tooling/runner/ProcessRunner {
}

public final class elide/tooling/runner/ProcessRunner$ProcessOptions : java/lang/Record {
public fun <init> (Lelide/tooling/runner/ProcessRunner$ProcessShell;)V
public fun <init> (Lelide/tooling/runner/ProcessRunner$ProcessShell;Ljava/nio/file/Path;)V
public final fun component1 ()Lelide/tooling/runner/ProcessRunner$ProcessShell;
public final fun copy (Lelide/tooling/runner/ProcessRunner$ProcessShell;)Lelide/tooling/runner/ProcessRunner$ProcessOptions;
public static synthetic fun copy$default (Lelide/tooling/runner/ProcessRunner$ProcessOptions;Lelide/tooling/runner/ProcessRunner$ProcessShell;ILjava/lang/Object;)Lelide/tooling/runner/ProcessRunner$ProcessOptions;
public final fun component2 ()Ljava/nio/file/Path;
public final fun copy (Lelide/tooling/runner/ProcessRunner$ProcessShell;Ljava/nio/file/Path;)Lelide/tooling/runner/ProcessRunner$ProcessOptions;
public static synthetic fun copy$default (Lelide/tooling/runner/ProcessRunner$ProcessOptions;Lelide/tooling/runner/ProcessRunner$ProcessShell;Ljava/nio/file/Path;ILjava/lang/Object;)Lelide/tooling/runner/ProcessRunner$ProcessOptions;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public final fun shell ()Lelide/tooling/runner/ProcessRunner$ProcessShell;
public fun toString ()Ljava/lang/String;
public final fun workingDirectory ()Ljava/nio/file/Path;
}

public abstract interface class elide/tooling/runner/ProcessRunner$ProcessShell {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public object ProcessRunner {
*/
@JvmRecord public data class ProcessOptions(
public val shell: ProcessShell,
public val workingDirectory: Path,
)

/**
Expand Down Expand Up @@ -128,6 +129,9 @@ public object ProcessRunner {
}
}
val procBuilder = ProcessBuilder(resolvedArgs).apply {
// handle working directory
directory(task.options.workingDirectory.toFile())

// handle task environment
when (task.env) {
// inject host environment
Expand Down Expand Up @@ -185,7 +189,10 @@ public object ProcessRunner {
val mutEnv = env.toMutable()
var executablePath: Path = exec
var effectiveStreams: StdStreams = streams ?: StdStreams.Defaults
var effectiveOptions: ProcessOptions = options ?: ProcessOptions(ProcessShell.None)
var effectiveOptions: ProcessOptions = options ?: ProcessOptions(
shell = ProcessShell.None,
workingDirectory = Path.of(System.getProperty("user.dir")),
)

return object : ProcessTaskBuilder {
override var executable: Path = executablePath
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2024-2025 Elide Technologies, Inc.
*
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://opensource.org/license/mit/
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under the License.
*/
package elide.tooling.runner

import java.nio.file.Path
import kotlin.test.*
import elide.tooling.runner.ProcessRunner.ProcessOptions
import elide.tooling.runner.ProcessRunner.ProcessShell

class ProcessRunnerTest {
@Test fun `ProcessOptions should allow specifying a custom working directory`() {
val customDir = Path.of("/tmp")
val options = ProcessOptions(
shell = ProcessShell.None,
workingDirectory = customDir,
)

assertEquals(customDir, options.workingDirectory)
}

@Test fun `ProcessRunner build should accept a custom working directory`() {
val customDir = Path.of("/tmp")
val exec = Path.of("/bin/ls")

val builder = ProcessRunner.build(exec) {
options = ProcessOptions(
shell = ProcessShell.None,
workingDirectory = customDir,
)
}

assertEquals(customDir, builder.options.workingDirectory)
assertEquals(exec, builder.executable)
}

@Test fun `ProcessRunner build should allow changing the working directory after building`() {
val initialDir = Path.of("/tmp")
val newDir = Path.of("/var")
val exec = Path.of("/bin/echo")

val builder = ProcessRunner.build(exec) {
options = ProcessOptions(
shell = ProcessShell.None,
workingDirectory = initialDir,
)
}

assertEquals(initialDir, builder.options.workingDirectory)

// Change the working directory
builder.options = ProcessOptions(
shell = ProcessShell.None,
workingDirectory = newDir,
)

assertEquals(newDir, builder.options.workingDirectory)
}

@Test fun `ProcessRunner buildFrom should use current working directory by default`() {
val exec = Path.of("/bin/echo")
val args = elide.tooling.Arguments.empty()
val env = elide.tooling.Environment.empty()

val builder = ProcessRunner.buildFrom(exec, args, env)

assertNotNull(builder.options.workingDirectory)
assertEquals(Path.of(System.getProperty("user.dir")), builder.options.workingDirectory)
}

@Test fun `ProcessRunner buildFrom should accept custom working directory`() {
val exec = Path.of("/bin/echo")
val args = elide.tooling.Arguments.empty()
val env = elide.tooling.Environment.empty()
val customDir = Path.of("/tmp")
val customOptions = ProcessOptions(
shell = ProcessShell.None,
workingDirectory = customDir,
)

val builder = ProcessRunner.buildFrom(exec, args, env, options = customOptions)

assertEquals(customDir, builder.options.workingDirectory)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2926,7 +2926,10 @@ internal class ToolShellCommand : ProjectAwareSubcommand<ToolState, CommandConte
env = Environment.HostEnv

// activate shell support
options = ProcessRunner.ProcessOptions(shell = ProcessRunner.ProcessShell.Active)
options = ProcessRunner.ProcessOptions(
shell = ProcessRunner.ProcessShell.Active,
workingDirectory = Path.of(System.getProperty("user.dir")),
)

// copy in the provided arguments
runnableArgs.takeIf { it.isNotEmpty() }?.let { arguments ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ object SubprocessRunner {
@JvmStatic suspend fun CommandContext.stringToTask(
spec: String,
shell: ProcessRunner.ProcessShell = ProcessRunner.ProcessShell.Active,
workingDirectory: Path = Path.of(System.getProperty("user.dir")),
): CommandLineProcessTaskBuilder {
val cwd = Path.of(System.getProperty("user.dir"))
val toolname = spec.substringBefore(' ')
val argsStr = spec.substringAfter(' ')
val argsArr = argsStr.split(' ')
Expand All @@ -69,7 +71,7 @@ object SubprocessRunner {
// use an identical path to elide so that versions always match.
// @TODO in jvm mode, this falls through and calls into native elide
toolname == "elide" && ImageInfo.inImageCode() -> Statics.binPath
toolname.startsWith(".") -> Path.of(System.getProperty("user.dir")).resolve(toolpath)
toolname.startsWith(".") -> cwd.resolve(toolpath)
else -> toolpath
}
suspend fun resolvedTool(): Path {
Expand All @@ -79,7 +81,7 @@ object SubprocessRunner {
which(resolvedToolpath) ?: resolvedToolpath
}
}
return subprocess(resolvedTool(), shell = shell) {
return subprocess(resolvedTool(), shell = shell, workingDirectory = workingDirectory) {
// add all cli args
args.addAllStrings(argsTrimmed.toList())
}
Expand Down Expand Up @@ -115,13 +117,12 @@ object SubprocessRunner {
@JvmStatic fun CommandContext.subprocess(
exec: Path,
shell: ProcessRunner.ProcessShell = ProcessRunner.ProcessShell.None,
workingDirectory: Path = Path.of(System.getProperty("user.dir")),
block: CommandLineProcessTaskBuilder.() -> Unit,
): CommandLineProcessTaskBuilder {
val out = ProcessRunner.build(exec) {
// by default, user shell access is off; if turned on, activate it from env
if (shell != ProcessRunner.ProcessShell.None) {
options = ProcessRunner.ProcessOptions(shell = shell)
}
// set process options with shell and working directory
options = ProcessRunner.ProcessOptions(shell = shell, workingDirectory = workingDirectory)

// default environment overrides PATH
env = Environment.HostEnv.extend(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2024-2025 Elide Technologies, Inc.
*
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://opensource.org/license/mit/
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under the License.
*/
package elide.tool.exec

import java.nio.file.Path
import kotlin.test.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import elide.tool.cli.CommandContext
import elide.tool.cli.state.CommandOptions
import elide.tool.cli.state.CommandState
import elide.tool.exec.SubprocessRunner.stringToTask
import elide.tool.exec.SubprocessRunner.subprocess
import elide.tooling.runner.ProcessRunner

class SubprocessRunnerTest {
private fun testContext(): CommandContext {
val options = CommandOptions.of(
args = emptyList(),
debug = false,
verbose = false,
quiet = false,
pretty = false,
)
val state = CommandState.of(options)
return CommandContext.default(state, Dispatchers.Default)
}

@Test fun `stringToTask should use current working directory by default`() = runTest {
val expectedCwd = Path.of(System.getProperty("user.dir"))
val context = testContext()

with(context) {
val builder = stringToTask("echo test")
assertEquals(expectedCwd, builder.options.workingDirectory)
}
}

@Test fun `stringToTask should accept custom working directory`() = runTest {
val customDir = Path.of("/tmp")
val context = testContext()

with(context) {
val builder = stringToTask("echo test", workingDirectory = customDir)
assertEquals(customDir, builder.options.workingDirectory)
}
}

@Test fun `should use current working directory by default when creating subprocess`() {
val expectedCwd = Path.of(System.getProperty("user.dir"))
val context = testContext()
val exec = Path.of("/bin/echo")

with(context) {
val builder = subprocess(exec) {}
assertEquals(expectedCwd, builder.options.workingDirectory)
}
}

@Test fun `should be able to pass custom working directory when creating subprocess`() {
val customDir = Path.of("/var")
val context = testContext()
val exec = Path.of("/bin/ls")

with(context) {
val builder = subprocess(exec, workingDirectory = customDir) {}
assertEquals(customDir, builder.options.workingDirectory)
}
}

@Test fun `should be able to pass custom working directory through to ProcessRunner options`() {
val customDir = Path.of("/usr")
val context = testContext()
val exec = Path.of("/bin/pwd")

with(context) {
val builder = subprocess(exec, workingDirectory = customDir) {
args.addAllStrings(listOf("arg1"))
}

assertEquals(customDir, builder.options.workingDirectory)
assertEquals(exec, builder.executable)
assertTrue(builder.args.asArgumentList().contains("arg1"))
}
}
}
Loading