mirror of
https://github.com/apple/pkl.git
synced 2026-01-11 14:20:35 +01:00
444 lines
14 KiB
Plaintext
444 lines
14 KiB
Plaintext
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2024-2025 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.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// A manifest that defines the presence of a project.
|
|
///
|
|
/// A project is useful for defining [dependencies], and also for defining common evaluator
|
|
/// [settings].
|
|
///
|
|
/// A project is a directory that contains a project file at its root.
|
|
/// The project file is a file named `PklProject` exactly, that amends or embeds `"pkl:Project"`.
|
|
///
|
|
/// When evaluating from the CLI, the project directory can be specified explicitly using
|
|
/// `--project-dir` flag, or is determined implicitly by walking up the working directory, looking
|
|
/// for a directory containing a `PklProject` file.
|
|
///
|
|
/// When using the API libraries (e.g. _pkl-swift_, _pkl-go_ or _pkl-config-java_), the project must be
|
|
/// explicitly stated via the evaluator builder.
|
|
///
|
|
/// ## Embedding [Project]
|
|
///
|
|
/// In a project file, instead of amending `pkl:Project`, it is possible simply set the
|
|
/// `output.value` property of a module to an instance of [Project].
|
|
/// This allows defining higher levels of abstraction that encapsulate an underlying [Project]
|
|
/// definition.
|
|
///
|
|
/// When defining an abstraction, it is important to:
|
|
/// 1. Set the module's `output.value` to the underlying project output.
|
|
/// 2. Set [projectFileUri] to the enclosing module's URI.
|
|
/// This is necessary to determine the correct project directory, as well as for resolving
|
|
/// local project dependencies correctly.
|
|
///
|
|
/// The [newInstance()] helper method exists as a convenient way to set this when embedding
|
|
/// project definitions.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// module MyTeamProject
|
|
///
|
|
/// import "pkl:Project"
|
|
///
|
|
/// /// The package name, to be prefixed with `myteam`.
|
|
/// packageName: String
|
|
///
|
|
/// project: Project = (Project.newInstance(module)) {
|
|
/// package {
|
|
/// name = "myteam.\(packageName)"
|
|
/// }
|
|
/// }
|
|
///
|
|
/// output {
|
|
/// value = project
|
|
/// }
|
|
/// ```
|
|
@ModuleInfo { minPklVersion = "0.31.0" }
|
|
module pkl.Project
|
|
|
|
import "pkl:EvaluatorSettings" as EvaluatorSettingsModule
|
|
import "pkl:Project"
|
|
import "pkl:reflect"
|
|
import "pkl:semver"
|
|
|
|
/// The details for the package represented by this project.
|
|
///
|
|
/// This section is used if publishing this project as a package.
|
|
package: Package?
|
|
|
|
/// The tests of the project.
|
|
///
|
|
/// If set, allows running `pkl test` without specifying the paths to source modules.
|
|
///
|
|
/// Relative paths are resolved against PklProject's enclosing directory.
|
|
///
|
|
/// Glob imports can be useful for defining this property.
|
|
///
|
|
/// Example:
|
|
///
|
|
/// ```
|
|
/// tests = import*("**.test.pkl").keys.toListing()
|
|
/// ```
|
|
tests: Listing<String>(isDistinct)
|
|
|
|
/// Tells if the project is a local module named `PklProject`, is not self, and has a [package] section
|
|
local isValidLoadDependency = (it: Project) ->
|
|
isUriLocal(projectFileUri, it.projectFileUri)
|
|
&& it.projectFileUri.endsWith("/PklProject")
|
|
&& it != module
|
|
&& it.package != null
|
|
|
|
local const function isUriLocal(uri1: Uri, uri2: Uri): Boolean =
|
|
// This is an imperfect check; should also check that the URIs have the same authority.
|
|
// We should improve this if/when there is a URI library in the stdlib.
|
|
uri1.substring(0, uri1.indexOf(":")) == uri2.substring(0, uri2.indexOf(":"))
|
|
|
|
/// The dependencies of this project.
|
|
///
|
|
/// A dependency is a group of Pkl modules and file resources that can be imported within the
|
|
/// project.
|
|
/// Within the project, a dependency can be referenced via _dependency notation_, where the name
|
|
/// is prefixed with the `@` character.
|
|
///
|
|
/// For example, given the following descriptor:
|
|
///
|
|
/// ```
|
|
/// dependencies {
|
|
/// ["birds"] {
|
|
/// uri = "package://example.com/birds@1.0.0"
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// This enables the following snippet:
|
|
///
|
|
/// ```
|
|
/// import "@birds/canary.pkl" // Import a module from the `birds` dependency
|
|
///
|
|
/// birdIndex = read("@birds/index.txt") // Read a file from the `birds` dependency
|
|
/// ```
|
|
///
|
|
/// A dependency's coordinates can either be specified in the form of a [RemoteDependency]
|
|
/// descriptor, which gets fetched over the network, or an import of another PklProject file, which
|
|
/// represents a package that exists locally.
|
|
///
|
|
/// Remote dependencies are fetched over HTTPS.
|
|
/// When fetching a remote dependency, two HTTPS requests are made.
|
|
/// Given dependency URI `package://example.com/foo@0.5.0`, the following requests are made:
|
|
///
|
|
/// 1. `GET https://example.com/foo@0.5.0` to retrieve the metadata JSON file.
|
|
/// 2. Given the metadata JSON file, make a GET request to the package URI ZIP archive.
|
|
///
|
|
/// If this project is published as a package, these dependencies are included within the published
|
|
/// package metadata.
|
|
///
|
|
/// ## Local project dependencies
|
|
///
|
|
/// A local project can alternatively be used as a dependency.
|
|
/// This is useful when structuring a single repository that publishes multiple packages.
|
|
///
|
|
/// To specify a local project dependency, import the relative `PklProject` file.
|
|
///
|
|
/// The local project dependency must define its own [package].
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// dependencies {
|
|
/// ["penguins"] = import("../penguins/PklProject")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## Resolving dependencies
|
|
///
|
|
/// Dependencies must be _resolved_ before they can be used.
|
|
/// To resolve dependencies, run the CLI command `pkl project resolve`.
|
|
/// This will generate a `PklProject.deps.json` file next to the `PklProject` file.
|
|
///
|
|
/// ### Minimum version selection
|
|
///
|
|
/// Pkl uses the
|
|
/// [minimum version selection algorithm 1](https://research.swtch.com/vgo-mvs#algorithm_1)
|
|
/// to resolve dependencies.
|
|
/// A dependency is identified by its package URI, as well as its major semver number.
|
|
///
|
|
/// To determine the resolved dependencies of a project, the following algorithm is applied:
|
|
/// 1. Gather all dependencies, both direct and transitive.
|
|
/// 2. For each package's major version, determine the highest declared minor version.
|
|
/// 3. Write each resolved dependency to sibling file `PklProject.deps.json`.
|
|
dependencies: Mapping<String(!contains("/")), *RemoteDependency | Project(isValidLoadDependency)>
|
|
|
|
local isFileBasedProject = projectFileUri.startsWith("file:")
|
|
|
|
/// If set, controls the base evaluator settings when running the evaluator.
|
|
///
|
|
/// These settings influence the behavior of the evaluator when running the `pkl eval`, `pkl test`,
|
|
/// and `pkl repl` CLI commands.
|
|
/// Command line flags passed to the CLI will override any settings defined here.
|
|
///
|
|
/// Other integrations can possibly ignore these evaluator settings.
|
|
///
|
|
/// Evaluator settings do not get published as part of a package.
|
|
/// It is not possible for a package dependency to influence the evaluator settings of a project.
|
|
///
|
|
/// The following values can only be set if this is a file-based project.
|
|
///
|
|
/// - [modulePath][EvaluatorSettings.modulePath]
|
|
/// - [rootDir][EvaluatorSettings.rootDir]
|
|
/// - [moduleCacheDir][EvaluatorSettings.moduleCacheDir]
|
|
///
|
|
/// For each of these, relative paths are resolved against the project's enclosing directory.
|
|
evaluatorSettings: EvaluatorSettingsModule(
|
|
(modulePath != null).implies(isFileBasedProject),
|
|
(rootDir != null).implies(isFileBasedProject),
|
|
(moduleCacheDir != null).implies(isFileBasedProject),
|
|
)
|
|
|
|
/// The URI of the PklProject file.
|
|
///
|
|
/// This value is used to resolve relative paths when importing another local project as a
|
|
/// dependency.
|
|
projectFileUri: String = reflect.Module(module).uri
|
|
|
|
/// Instantiates a project definition within the enclosing module.
|
|
///
|
|
/// This is a convenience method for setting [projectFileUri] to the enclosing module's URI.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// myProject: Project = (Project.newInstance(module)) {
|
|
/// dependencies { /* etc */ }
|
|
/// }
|
|
/// ```
|
|
function newInstance(enclosingModule: Module): Project = new {
|
|
projectFileUri = reflect.Module(enclosingModule).uri
|
|
}
|
|
|
|
local const hasVersion = (it: Uri) ->
|
|
let (versionSep = it.lastIndexOf("@"))
|
|
if (versionSep == -1)
|
|
false
|
|
else
|
|
let (version = it.drop(versionSep + 1)) semver.parseOrNull(version) != null
|
|
|
|
typealias PackageUri = Uri(startsWith("package:"), hasVersion)
|
|
|
|
class RemoteDependency {
|
|
/// The URI that this dependency is published to.
|
|
uri: PackageUri
|
|
|
|
/// The checksums of this package.
|
|
///
|
|
/// If omitted, this is taken from the derived package coordinates.
|
|
checksums: Checksums?
|
|
}
|
|
|
|
class Checksums {
|
|
/// The [SHA-256](https://en.wikipedia.org/wiki/SHA-2) checksum value of the dependency, in hexadecimal representation.
|
|
sha256: String
|
|
}
|
|
|
|
/// An email address, conformant to the
|
|
/// [RFC5322 mailbox](https://www.rfc-editor.org/rfc/rfc5322#section-3.4) specification.
|
|
///
|
|
/// Can be in the form of an address spec, or a named address.
|
|
///
|
|
/// Examples:
|
|
/// * `"johnny.appleseed@example.com"`
|
|
/// * `"Johnny Appleseed <johnny.appleseed@example.com>"`
|
|
typealias EmailAddress = String(matches(Regex(#".+@\S+|.+<\S+@\S+>"#)))
|
|
|
|
class Package {
|
|
/// The name of this package.
|
|
///
|
|
/// The package name is only used for display purposes.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// name = "myproject"
|
|
/// ```
|
|
name: String
|
|
|
|
/// The URI that the package is published to, without the version part.
|
|
///
|
|
/// This, along with the version, determines the import path for modules and resources published
|
|
/// by this package.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// baseUri = "package://example.com/myproject"
|
|
/// ```
|
|
baseUri: Uri(startsWith("package:"))
|
|
|
|
/// The version of this package.
|
|
///
|
|
/// Must adhere to semantic versioning.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// version = "1.5.0"
|
|
/// ```
|
|
version: String(semver.isValid(this))
|
|
|
|
/// The HTTPS location for the zip archive for this package.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// packageZipUrl = "https://example.com/artifacts/myproject/\(version).zip"
|
|
/// ```
|
|
packageZipUrl: Uri(startsWith("https:"))
|
|
|
|
/// The description of this package.
|
|
description: String?
|
|
|
|
/// The maintainers' emails for this package.
|
|
///
|
|
/// Email addresses must adhere to
|
|
/// [RFC5322 mailbox](https://www.rfc-editor.org/rfc/rfc5322#section-3.4) specification.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// email { "Johnny Appleseed <johnny.appleseed@example.com>" }
|
|
/// ```
|
|
authors: Listing<EmailAddress>
|
|
|
|
/// The website for this package.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// website = "https://example.com/myproject"
|
|
/// ```
|
|
website: String?
|
|
|
|
/// The web URL of the Pkldoc documentation for this package.
|
|
documentation: Uri(!endsWith("/"))?
|
|
|
|
/// The source code repository for this package.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// sourceCode = "https://github.com/myorg/myproject"
|
|
/// ```
|
|
sourceCode: String?
|
|
|
|
/// The source code scheme for this package.
|
|
///
|
|
/// This is used to transform stack frames for errors arising for this package.
|
|
///
|
|
/// The following placeholders are available:
|
|
///
|
|
/// - `%{path}`
|
|
/// absolute file path of the file to open
|
|
/// - `%{line}`
|
|
/// start line number to navigate to
|
|
/// - `%{endLine}`
|
|
/// end line number to navigate to
|
|
/// - `%{column}`
|
|
/// column number to navigate to
|
|
/// - `%{endColumn}`
|
|
/// end column number to navigate to
|
|
///
|
|
/// For example, if publishing to GitHub, assuming that the version gets published as a tag:
|
|
///
|
|
/// ```
|
|
/// sourceCodeUrlScheme = "\(sourceCode)/blob/\(version)%{path}#L%{line}-L%{endLine}"
|
|
/// ```
|
|
sourceCodeUrlScheme: String?
|
|
|
|
/// The license associated with this package.
|
|
///
|
|
/// If using a common license, use its [SPDX license identifier](https://spdx.org/licenses/).
|
|
///
|
|
/// If using multiple common licenses, use a
|
|
/// [SPDX license expression syntax version 2.0 string](https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/).
|
|
/// For example: `"Apache-2.0 or MIT"`.
|
|
///
|
|
/// If using an uncommon license, also provide its full text in the [licenseText] property.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// license = "Apache-2.0"
|
|
/// ```
|
|
license: (CommonSpdxLicenseIdentifier | String)?
|
|
|
|
/// The full text of the license associated with this package.
|
|
licenseText: String?
|
|
|
|
/// The web URL of the issue tracker for this package.
|
|
issueTracker: String?
|
|
|
|
/// Paths to the tests that define the API of the package.
|
|
///
|
|
/// These tests are run as part of the `pkl project package` command.
|
|
///
|
|
/// Relative paths are resolved against PklProject's enclosing directory.
|
|
///
|
|
/// Glob imports can be useful for defining this property.
|
|
///
|
|
/// Example:
|
|
///
|
|
/// ```
|
|
/// tests = import*("**.test.pkl").keys.toListing()
|
|
/// ```
|
|
apiTests: Listing<String>(isDistinct)
|
|
|
|
/// Glob patterns describing the set of files to exclude from packaging.
|
|
///
|
|
/// By default, the project manifest files are excluded, and any paths that start with a dot.
|
|
///
|
|
/// Glob patterns follows the same glob rules as glob imports and reads.
|
|
exclude: Listing<String> = new {
|
|
"PklProject"
|
|
"PklProject.deps.json"
|
|
".**"
|
|
}
|
|
|
|
/// The effective package URI for the package represented by this project.
|
|
fixed uri: PackageUri = "\(baseUri)@\(version)"
|
|
}
|
|
|
|
@Deprecated { since = "0.26.0"; replaceWith = "EvaluatorSettingsModule" }
|
|
typealias EvaluatorSettings = EvaluatorSettingsModule
|
|
|
|
/// Common software licenses in the [SPDX License List](https://spdx.org/licenses/).
|
|
typealias CommonSpdxLicenseIdentifier =
|
|
"Apache-2.0"
|
|
| "MIT"
|
|
| "BSD-2-Clause"
|
|
| "BSD-3-Clause"
|
|
| "ISC"
|
|
| "GPL-3.0"
|
|
| "GPL-2.0"
|
|
| "MPL-2.0"
|
|
| "MPL-1.1"
|
|
| "MPL-1.0"
|
|
| "AGPL-1.0-only"
|
|
| "AGPL-1.0-or-later"
|
|
| "AGPL-3.0-only"
|
|
| "AGPL-3.0-or-later"
|
|
| "LGPL-2.0-only"
|
|
| "LGPL-2.0-or-later"
|
|
| "LGPL-2.1-only"
|
|
| "LGPL-2.1-or-later"
|
|
| "LGPL-3.0-only"
|
|
| "LGPL-3.0-or-later"
|
|
| "EPL-1.0"
|
|
| "EPL-2.0"
|
|
| "UPL-1.0"
|
|
| "BSL-1.0"
|
|
| "Unlicense"
|
|
|
|
@Unlisted
|
|
@Since { version = "0.27.0" }
|
|
fixed annotations: List<Annotation> = reflect.moduleOf(this).annotations
|