mirror of
https://github.com/apple/pkl.git
synced 2026-03-28 11:51:58 +01:00
Added support for an alternative current dir mode in pkldoc (#824)
Some systems have trouble with handling symlinks, which breaks the current directory links created by Pkldoc. In this PR, we add an alternative mode which creates a full copy of the latest published version contents in the current directory instead. Co-authored-by: Dan Chao <dan.chao@apple.com>
This commit is contained in:
@@ -232,10 +232,18 @@ Relative URIs are resolved against the working directory.
|
||||
[%collapsible]
|
||||
====
|
||||
Default: (none) +
|
||||
Example: `pkldoc`
|
||||
Example: `pkldoc` +
|
||||
The directory where generated documentation is placed.
|
||||
====
|
||||
|
||||
.--no-symlinks
|
||||
[%collapsible]
|
||||
====
|
||||
Create copies of files and directories instead of symbolic links.
|
||||
In particular, this affects how the "current" directories containing documentation content for the last generated version should be created.
|
||||
By default, a symbolic link is created pointing to the last generated version. If symlinks are disabled, a full copy of the last generated version is created.
|
||||
====
|
||||
|
||||
Common CLI options:
|
||||
|
||||
include::../../pkl-cli/partials/cli-common-options.adoc[]
|
||||
|
||||
@@ -534,6 +534,17 @@ Example: `outputDir = layout.projectDirectory.dir("pkl-docs")` +
|
||||
The directory where generated documentation is placed.
|
||||
====
|
||||
|
||||
.noSymlinks: Property<Boolean>
|
||||
[%collapsible]
|
||||
====
|
||||
Default: `false` +
|
||||
Example: `noSymlinks = true` +
|
||||
Create copies of files and directories instead of symbolic links.
|
||||
In particular, this affects how the "current" directories containing documentation content for the last generated version should be created.
|
||||
By default, a symbolic link is created pointing to the last generated version.
|
||||
If symlinks are disabled, a full copy of the last generated version is created.
|
||||
====
|
||||
|
||||
Common properties:
|
||||
|
||||
include::../partials/gradle-modules-properties.adoc[]
|
||||
|
||||
@@ -20,6 +20,9 @@ import java.nio.charset.Charset
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.util.stream.Stream
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import kotlin.io.path.exists
|
||||
|
||||
// not stored to avoid build-time initialization by native-image
|
||||
val currentWorkingDir: Path
|
||||
@@ -51,6 +54,19 @@ fun Path.writeString(
|
||||
@Throws(IOException::class)
|
||||
fun Path.readString(charset: Charset = Charsets.UTF_8): String = Files.readString(this, charset)
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun Path.copyRecursively(target: Path) {
|
||||
if (exists()) {
|
||||
target.createParentDirectories()
|
||||
walk().use { paths ->
|
||||
paths.forEach { src ->
|
||||
val dst = target.resolve(this@copyRecursively.relativize(src))
|
||||
src.copyTo(dst, overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isWindows by lazy { System.getProperty("os.name").contains("Windows") }
|
||||
|
||||
/** Copy implementation from IoUtils.toNormalizedPathString */
|
||||
|
||||
@@ -270,6 +270,7 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
|
||||
versionComparator,
|
||||
options.normalizedOutputDir,
|
||||
options.isTestMode,
|
||||
options.noSymlinks,
|
||||
)
|
||||
.run()
|
||||
} catch (e: DocGeneratorException) {
|
||||
|
||||
@@ -36,6 +36,17 @@ constructor(
|
||||
* files (e.g., when stdlib line numbers change).
|
||||
*/
|
||||
val isTestMode: Boolean = false,
|
||||
|
||||
/**
|
||||
* Disables creation of symlinks, replacing them with regular files and directories.
|
||||
*
|
||||
* In particular, this affects creation of the "current" directory which contains documentation
|
||||
* for the latest version of the package.
|
||||
*
|
||||
* `false` will make the current directory into a symlink to the actual version directory. `true`,
|
||||
* however, will create a full copy instead.
|
||||
*/
|
||||
val noSymlinks: Boolean = false,
|
||||
) {
|
||||
/** [outputDir] after undergoing normalization. */
|
||||
val normalizedOutputDir: Path = base.normalizedWorkingDir.resolveSafely(outputDir)
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
import org.pkl.commons.copyRecursively
|
||||
import org.pkl.core.ModuleSchema
|
||||
import org.pkl.core.PClassInfo
|
||||
import org.pkl.core.Version
|
||||
@@ -50,6 +51,7 @@ class DocGenerator(
|
||||
|
||||
/** A comparator for package versions. */
|
||||
versionComparator: Comparator<String>,
|
||||
|
||||
/** The directory where generated documentation is placed. */
|
||||
private val outputDir: Path,
|
||||
|
||||
@@ -60,8 +62,21 @@ class DocGenerator(
|
||||
* files (e.g., when stdlib line numbers change).
|
||||
*/
|
||||
private val isTestMode: Boolean = false,
|
||||
|
||||
/**
|
||||
* Disables creation of symbolic links, using copies of files and directories instead of them.
|
||||
*
|
||||
* In particular, determines how to create the "current" directory which contains documentation
|
||||
* for the latest version of the package.
|
||||
*
|
||||
* `false` will make the current directory into a symlink to the actual version directory. `true`,
|
||||
* however, will create a full copy instead.
|
||||
*/
|
||||
private val noSymlinks: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
const val CURRENT_DIRECTORY_NAME = "current"
|
||||
|
||||
internal fun List<PackageData>.current(
|
||||
versionComparator: Comparator<String>
|
||||
): List<PackageData> {
|
||||
@@ -102,7 +117,7 @@ class DocGenerator(
|
||||
val packagesData = packageDataGenerator.readAll()
|
||||
val currentPackagesData = packagesData.current(descendingVersionComparator)
|
||||
|
||||
createSymlinks(currentPackagesData)
|
||||
createCurrentDirectories(currentPackagesData)
|
||||
|
||||
htmlGenerator.generateSite(currentPackagesData)
|
||||
searchIndexGenerator.generateSiteIndex(currentPackagesData)
|
||||
@@ -117,14 +132,21 @@ class DocGenerator(
|
||||
outputDir.resolve(IoUtils.encodePath("$name/$version")).deleteRecursively()
|
||||
}
|
||||
|
||||
private fun createSymlinks(currentPackagesData: List<PackageData>) {
|
||||
private fun createCurrentDirectories(currentPackagesData: List<PackageData>) {
|
||||
for (packageData in currentPackagesData) {
|
||||
val basePath = outputDir.resolve(packageData.ref.pkg.pathEncoded)
|
||||
val src = basePath.resolve(packageData.ref.version)
|
||||
val dest = basePath.resolve("current")
|
||||
if (dest.exists() && dest.isSameFileAs(src)) continue
|
||||
dest.deleteIfExists()
|
||||
dest.createSymbolicLinkPointingTo(IoUtils.relativize(src, basePath))
|
||||
val dst = basePath.resolve(CURRENT_DIRECTORY_NAME)
|
||||
|
||||
if (noSymlinks) {
|
||||
dst.deleteRecursively()
|
||||
src.copyRecursively(dst)
|
||||
} else {
|
||||
if (!dst.exists() || !dst.isSameFileAs(src)) {
|
||||
dst.deleteRecursively()
|
||||
dst.createSymbolicLinkPointingTo(IoUtils.relativize(src, basePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.arguments.convert
|
||||
import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.required
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
@@ -30,6 +31,7 @@ import org.pkl.commons.cli.cliMain
|
||||
import org.pkl.commons.cli.commands.BaseCommand
|
||||
import org.pkl.commons.cli.commands.BaseOptions.Companion.parseModuleName
|
||||
import org.pkl.commons.cli.commands.ProjectOptions
|
||||
import org.pkl.commons.cli.commands.single
|
||||
import org.pkl.core.Release
|
||||
|
||||
/** Main method for the Pkldoc CLI. */
|
||||
@@ -57,11 +59,24 @@ class DocCommand :
|
||||
.path()
|
||||
.required()
|
||||
|
||||
private val noSymlinks: Boolean by
|
||||
option(
|
||||
names = arrayOf("--no-symlinks"),
|
||||
help = "Create copies of directories and files instead of symbolic links.",
|
||||
)
|
||||
.single()
|
||||
.flag(default = false)
|
||||
|
||||
private val projectOptions by ProjectOptions()
|
||||
|
||||
override fun run() {
|
||||
val options =
|
||||
CliDocGeneratorOptions(baseOptions.baseOptions(modules, projectOptions), outputDir, true)
|
||||
CliDocGeneratorOptions(
|
||||
baseOptions.baseOptions(modules, projectOptions),
|
||||
outputDir,
|
||||
true,
|
||||
noSymlinks,
|
||||
)
|
||||
CliDocGenerator(options).run()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.pkl.doc
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
@@ -35,13 +36,14 @@ import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.commons.test.PackageServer
|
||||
import org.pkl.commons.test.listFilesRecursively
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.commons.walk
|
||||
import org.pkl.core.Version
|
||||
import org.pkl.core.util.IoUtils
|
||||
import org.pkl.doc.DocGenerator.Companion.current
|
||||
|
||||
class CliDocGeneratorTest {
|
||||
companion object {
|
||||
private val tempFileSystem by lazy { Jimfs.newFileSystem(Configuration.unix()) }
|
||||
private val tempFileSystem: FileSystem by lazy { Jimfs.newFileSystem(Configuration.unix()) }
|
||||
|
||||
private val tmpOutputDir by lazy {
|
||||
tempFileSystem.getPath("/work/output").apply { createDirectories() }
|
||||
@@ -107,7 +109,7 @@ class CliDocGeneratorTest {
|
||||
|
||||
private val binaryFileExtensions = setOf("woff2", "png", "svg")
|
||||
|
||||
private fun runDocGenerator(outputDir: Path, cacheDir: Path?) {
|
||||
private fun runDocGenerator(outputDir: Path, cacheDir: Path?, noSymlinks: Boolean = false) {
|
||||
CliDocGenerator(
|
||||
CliDocGeneratorOptions(
|
||||
CliBaseOptions(
|
||||
@@ -125,6 +127,7 @@ class CliDocGeneratorTest {
|
||||
),
|
||||
outputDir = outputDir,
|
||||
isTestMode = true,
|
||||
noSymlinks = noSymlinks,
|
||||
)
|
||||
)
|
||||
.run()
|
||||
@@ -254,15 +257,35 @@ class CliDocGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates a symlink called current`(@TempDir tempDir: Path) {
|
||||
fun `creates a symlink called current by default`(@TempDir tempDir: Path) {
|
||||
PackageServer.populateCacheDir(tempDir)
|
||||
runDocGenerator(actualOutputDir, tempDir)
|
||||
|
||||
val expectedSymlink = actualOutputDir.resolve("com.package1/current")
|
||||
val expectedDestination = actualOutputDir.resolve("com.package1/1.2.3")
|
||||
org.junit.jupiter.api.Assertions.assertTrue(Files.isSymbolicLink(expectedSymlink))
|
||||
org.junit.jupiter.api.Assertions.assertTrue(
|
||||
Files.isSameFile(expectedSymlink, expectedDestination)
|
||||
)
|
||||
|
||||
assertThat(expectedSymlink).isSymbolicLink().matches {
|
||||
Files.isSameFile(it, expectedDestination)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates a copy of the latest output called current when symlinks are disabled`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
PackageServer.populateCacheDir(tempDir)
|
||||
runDocGenerator(actualOutputDir, tempDir, noSymlinks = true)
|
||||
|
||||
val currentDirectory = actualOutputDir.resolve("com.package1/current")
|
||||
val sourceDirectory = actualOutputDir.resolve("com.package1/1.2.3")
|
||||
|
||||
assertThat(currentDirectory).isDirectory()
|
||||
assertThat(currentDirectory.isSymbolicLink()).isFalse()
|
||||
|
||||
val expectedFiles = sourceDirectory.walk().map(sourceDirectory::relativize).toList()
|
||||
val actualFiles = currentDirectory.walk().map(currentDirectory::relativize).toList()
|
||||
|
||||
assertThat(actualFiles).hasSameElementsAs(expectedFiles)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -261,8 +261,14 @@ public class PklPlugin implements Plugin<Project> {
|
||||
.getBuildDirectory()
|
||||
.map(it -> it.dir("pkldoc").dir(spec.getName())));
|
||||
|
||||
spec.getNoSymlinks().convention(false);
|
||||
|
||||
createModulesTask(PkldocTask.class, spec)
|
||||
.configure(task -> task.getOutputDir().set(spec.getOutputDir()));
|
||||
.configure(
|
||||
task -> {
|
||||
task.getOutputDir().set(spec.getOutputDir());
|
||||
task.getNoSymlinks().set(spec.getNoSymlinks());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* 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.
|
||||
@@ -16,8 +16,11 @@
|
||||
package org.pkl.gradle.spec;
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
|
||||
/** Configuration options for Pkldoc generators. Documented in user manual. */
|
||||
public interface PkldocSpec extends ModulesSpec {
|
||||
DirectoryProperty getOutputDir();
|
||||
|
||||
Property<Boolean> getNoSymlinks();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
package org.pkl.gradle.task;
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.pkl.doc.CliDocGenerator;
|
||||
import org.pkl.doc.CliDocGeneratorOptions;
|
||||
@@ -24,11 +26,17 @@ public abstract class PkldocTask extends ModulesTask {
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDir();
|
||||
|
||||
@Input
|
||||
public abstract Property<Boolean> getNoSymlinks();
|
||||
|
||||
@Override
|
||||
protected void doRunTask() {
|
||||
new CliDocGenerator(
|
||||
new CliDocGeneratorOptions(
|
||||
getCliBaseOptions(), getOutputDir().get().getAsFile().toPath()))
|
||||
getCliBaseOptions(),
|
||||
getOutputDir().get().getAsFile().toPath(),
|
||||
false,
|
||||
getNoSymlinks().get()))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user