mirror of
https://github.com/apple/pkl.git
synced 2026-03-27 03:21:13 +01:00
Allow referring to remote project dependencies on the CLI with @-notation (#1434)
This commit is contained in:
@@ -175,11 +175,19 @@ data class CliBaseOptions(
|
||||
/** [rootDir] after normalization. */
|
||||
val normalizedRootDir: Path? = rootDir?.let(normalizedWorkingDir::resolve)
|
||||
|
||||
/** The effective project directory, if exists. */
|
||||
val normalizedProjectFile: Path? by lazy {
|
||||
projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
||||
?: normalizedWorkingDir.getProjectFile(rootDir)
|
||||
}
|
||||
|
||||
/** [sourceModules] after normalization. */
|
||||
val normalizedSourceModules: List<URI> =
|
||||
sourceModules
|
||||
.map { uri ->
|
||||
if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri)
|
||||
if (uri.isAbsolute) uri
|
||||
else if (uri.path.startsWith("@") && !noProject && normalizedProjectFile != null) uri
|
||||
else IoUtils.resolve(normalizedWorkingDir.toUri(), uri)
|
||||
}
|
||||
// sort modules to make cli output independent of source module order
|
||||
.sorted()
|
||||
@@ -195,12 +203,6 @@ data class CliBaseOptions(
|
||||
/** [moduleCacheDir] after normalization. */
|
||||
val normalizedModuleCacheDir: Path? = moduleCacheDir?.let(normalizedWorkingDir::resolve)
|
||||
|
||||
/** The effective project directory, if exists. */
|
||||
val normalizedProjectFile: Path? by lazy {
|
||||
projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
||||
?: normalizedWorkingDir.getProjectFile(rootDir)
|
||||
}
|
||||
|
||||
/** [caCertificates] after normalization. */
|
||||
val normalizedCaCertificates: List<Path> = caCertificates.map(normalizedWorkingDir::resolve)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,35 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun resolveModuleUri(uri: URI): URI =
|
||||
if (uri.isAbsolute) uri
|
||||
else { // must be @dep/mod.pkl notation!!
|
||||
if (!uri.path.startsWith('@'))
|
||||
throw CliBugException(
|
||||
RuntimeException("tried to resolve project URI `$uri` with no @ prefix")
|
||||
)
|
||||
if (project == null)
|
||||
throw CliBugException(
|
||||
RuntimeException("tried to resolve project URI `$uri` with no project present")
|
||||
)
|
||||
val dep = uri.path.substringBefore('/').drop(1)
|
||||
val path = uri.path.dropWhile { it != '/' }
|
||||
if (path.isEmpty()) throw CliException("Invalid project dependency URI `$uri`.")
|
||||
|
||||
val remoteDep =
|
||||
project!!.dependencies.remoteDependencies()[dep]
|
||||
?: if (project!!.dependencies.localDependencies().containsKey(dep))
|
||||
throw CliException(
|
||||
"Only remote project dependencies may be referenced using @-notation. Dependency `@$dep` is a local dependency."
|
||||
)
|
||||
else throw CliException("Project does not contain dependency `@$dep`.")
|
||||
remoteDep.packageUri.toPackageAssetUri(path).uri
|
||||
}
|
||||
|
||||
protected val resolvedSourceModules: List<URI> =
|
||||
if (project == null) cliOptions.normalizedSourceModules
|
||||
else cliOptions.normalizedSourceModules.map(::resolveModuleUri)
|
||||
|
||||
protected fun loadProject(projectFile: Path): Project {
|
||||
val securityManager =
|
||||
SecurityManagers.standard(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,9 +17,14 @@ package org.pkl.commons.cli.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.Context
|
||||
import com.github.ajalt.clikt.core.context
|
||||
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||
|
||||
abstract class BaseCommand(name: String, private val helpLink: String) : CliktCommand(name = name) {
|
||||
init {
|
||||
context { readArgumentFile = null }
|
||||
}
|
||||
|
||||
abstract val helpString: String
|
||||
|
||||
override fun help(context: Context) = helpString
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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 org.pkl.commons.cli.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.NoOpCliktCommand
|
||||
import com.github.ajalt.clikt.core.context
|
||||
|
||||
open class NoOpCommand(name: String? = null) : NoOpCliktCommand(name) {
|
||||
init {
|
||||
context { readArgumentFile = null }
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,18 @@
|
||||
package org.pkl.commons.cli
|
||||
|
||||
import com.github.ajalt.clikt.core.parse
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.writeText
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.cli.commands.BaseCommand
|
||||
import org.pkl.core.SecurityManagers
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
class CliCommandTest {
|
||||
|
||||
class CliTest(options: CliBaseOptions) : CliCommand(options) {
|
||||
@@ -28,6 +35,7 @@ class CliCommandTest {
|
||||
|
||||
val myAllowedResources = allowedResources
|
||||
val myAllowedModules = allowedModules
|
||||
val myResolvedSourceModules = resolvedSourceModules
|
||||
}
|
||||
|
||||
private val cmd =
|
||||
@@ -55,7 +63,7 @@ class CliCommandTest {
|
||||
"scheme+ext=reader5 with args",
|
||||
)
|
||||
)
|
||||
val opts = cmd.baseOptions.baseOptions(emptyList(), null, true)
|
||||
val opts = cmd.baseOptions.baseOptions(emptyList(), testMode = true)
|
||||
val cliTest = CliTest(opts)
|
||||
assertThat(cliTest.myAllowedModules.map { it.pattern() })
|
||||
.isEqualTo(
|
||||
@@ -68,4 +76,112 @@ class CliCommandTest {
|
||||
listOf("\\Qscheme1:\\E", "\\Qscheme2:\\E", "\\Qscheme+ext:\\E")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@-notation package URIs - treated as relative paths when no project present`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
cmd.parse(arrayOf("--working-dir=$tempDir"))
|
||||
val opts = cmd.baseOptions.baseOptions(listOf(URI("@foo/bar.pkl")), testMode = true)
|
||||
val cliTest = CliTest(opts)
|
||||
assertThat(cliTest.myResolvedSourceModules)
|
||||
.isEqualTo(listOf(tempDir.toUri().resolve("@foo/bar.pkl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@-notation package URIs - empty paths are rejected`(@TempDir tempDir: Path) {
|
||||
tempDir
|
||||
.resolve("PklProject")
|
||||
.writeText(
|
||||
"""
|
||||
amends "pkl:Project"
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
cmd.parse(arrayOf("--working-dir=$tempDir"))
|
||||
val opts = cmd.baseOptions.baseOptions(listOf(URI("@no.slash")), testMode = true)
|
||||
val exc = assertThrows<CliException> { CliTest(opts) }
|
||||
assertThat(exc.message).isEqualTo("Invalid project dependency URI `@no.slash`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@-notation package URIs - missing dependencies are rejected`(@TempDir tempDir: Path) {
|
||||
tempDir
|
||||
.resolve("PklProject")
|
||||
.writeText(
|
||||
"""
|
||||
amends "pkl:Project"
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
cmd.parse(arrayOf("--working-dir=$tempDir"))
|
||||
val opts = cmd.baseOptions.baseOptions(listOf(URI("@foo/bar.pkl")), testMode = true)
|
||||
val exc = assertThrows<CliException> { CliTest(opts) }
|
||||
assertThat(exc.message).isEqualTo("Project does not contain dependency `@foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@-notation package URIs - local dependencies are rejected`(
|
||||
@TempDir tempDir: Path,
|
||||
@TempDir depDir: Path,
|
||||
) {
|
||||
depDir
|
||||
.resolve("PklProject")
|
||||
.writeText(
|
||||
"""
|
||||
amends "pkl:Project"
|
||||
|
||||
package {
|
||||
name = "foo"
|
||||
baseUri = "package://example.com/foo"
|
||||
version = "0.0.1"
|
||||
packageZipUrl = "https://example.com/foo@\(version).zip"
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
tempDir
|
||||
.resolve("PklProject")
|
||||
.writeText(
|
||||
"""
|
||||
amends "pkl:Project"
|
||||
|
||||
dependencies {
|
||||
["foo"] = import("$depDir/PklProject")
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
cmd.parse(arrayOf("--working-dir=$tempDir"))
|
||||
val opts = cmd.baseOptions.baseOptions(listOf(URI("@foo/bar.pkl")), testMode = true)
|
||||
val exc = assertThrows<CliException> { CliTest(opts) }
|
||||
assertThat(exc.message)
|
||||
.isEqualTo(
|
||||
"Only remote project dependencies may be referenced using @-notation. Dependency `@foo` is a local dependency."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@-notation package URIs - remote dependencies are resolved`(@TempDir tempDir: Path) {
|
||||
tempDir
|
||||
.resolve("PklProject")
|
||||
.writeText(
|
||||
"""
|
||||
amends "pkl:Project"
|
||||
|
||||
dependencies {
|
||||
["foo"] {
|
||||
uri = "package://example.com/foo@1.2.3"
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
cmd.parse(arrayOf("--working-dir=$tempDir"))
|
||||
val opts = cmd.baseOptions.baseOptions(listOf(URI("@foo/bar.pkl")), testMode = true)
|
||||
val cliTest = CliTest(opts)
|
||||
assertThat(cliTest.myResolvedSourceModules)
|
||||
.isEqualTo(listOf(tempDir.toUri().resolve("package://example.com/foo@1.2.3#/bar.pkl")))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user