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:
Vladimir Matveev
2025-02-19 08:52:32 -08:00
committed by GitHub
parent 2ffd201172
commit baa34a6dd1
11 changed files with 143 additions and 19 deletions

View File

@@ -270,6 +270,7 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
versionComparator,
options.normalizedOutputDir,
options.isTestMode,
options.noSymlinks,
)
.run()
} catch (e: DocGeneratorException) {

View File

@@ -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)

View File

@@ -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))
}
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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