Initial files
0 parent commit c948355b6a8dd4f53c2e6f663e83677979554440
Nigel Stanger authored on 18 Mar 2022
Showing 4 changed files
View
30
README.md 0 → 100644
# Gradle build infrastructure for lectures
 
Gradle infrastructure to build Information Science lectures.
 
Features:
* multiple configurable document targets: slides, handout, notes, examples
* dynamic targets for building PDF images from PlantUML, SVG, R
 
This repository serves as both the canonical source for the Gradle files and as an example of how to set things up.
 
## Setting up a new set of lectures
 
1. Create a top level `lectures` directory.
2. Create a sub-directory for each lecture, e.g., `lecture1`, `lecture2`, ….
3. Copy `build.gradle.kts` and `settings.gradle.kts` into `lectures`.
4. Create an empty `settings.gradle.kts` in `lecture1`, `lecture2`, etc.
5. Copy `lecture.gradle.kts` into `lecture1`, `lecture2`, etc.
6. Edit each `lecture.gradle.kts` to configure targets and images.
 
## Standard lecture directory tree
 
```sh
lectures
lecture1
images
pdfs
lecture2
...
```
View
391
build.gradle.kts 0 → 100644
/***********************************************************************
* CONFIGURABLE SETTINGS
*
* Override the following values in lecture.gradle.kts.
* In most cases simply adding a modified copy of the original line
* should do what you need. Most collections are sets to avoid accidental
* duplication. You can append to an existing set value by doing something
* like this (e.g.):
*
* extra["latexFlags"] = (extra["latexFlags"] as Set<String>) + "--barf"
***********************************************************************/
 
/***********************************************************************
* Supported targets (unnumbered documents).
* Remove any targets that don't exist for this lecture.
* New targets must be registered in lecture.gradle.kts.
* The target list for numbered documents is automatically derived from
* this (see below).
*/
val docTargets by extra(mutableSetOf<String>(
"slides",
"handout",
"notes",
"examples"
))
 
/***********************************************************************
* Command line options for LaTeX. This is a set, so duplicates will be
* automatically eliminated.
*/
val latexFlags by extra(mutableSetOf<String>(
"-shell-escape",
"-synctex=1",
"-interaction=nonstopmode",
"-halt-on-error",
"-file-line-error"
))
 
/***********************************************************************
* Standard document names (unnumbered). The keys are mostly the Gradle
* target names in docTargets, plus "content".
*/
val docNames by extra(mutableMapOf<String, String>(
"content" to "lecture_content",
"slides" to "lecture_slides",
"handout" to "lecture_handout",
"notes" to "lecture_notes",
"examples" to "lecture_examples"
))
 
/***********************************************************************
* Lecture number, derived from the project name (assumes the project
* folder is named something like "01_Lecture_Title").
*/
val lectureNum by extra(project.name.substring(0, 2))
 
/***********************************************************************
* Name of subdirectory in which images are kept.
*/
val imageDir by extra(layout.projectDirectory.dir("images"))
 
/***********************************************************************
* Name of subdirectory to which numbered PDFs will be written. Originally
* These were written into the project directory, but it seemed to completely
* confuse Gradle into thinking that all the input files in the project
* has been touched and it yelled about implicit task dependencies. See
* <https://docs.gradle.org/7.3.1/userguide/validation_problems.html#implicit_dependency>.
*/
val pdfDir by extra(layout.projectDirectory.dir("pdfs"))
 
/***********************************************************************
* Images and files used by the slides, handout, and notes documents.
* Images will be automatically mapped to imageDir (above).
*/
val slidesImages by extra(mutableSetOf<String>())
val slidesFiles by extra(mutableSetOf<String>(
"${docNames["content"]}.tex",
"paper_init.tex",
"lecturedates.tex",
"doc_init.tex"
))
 
/***********************************************************************
* Images and files used by the examples document.
* Images will be automatically mapped to imageDir (above).
*/
val examplesImages by extra(mutableSetOf<String>())
val examplesFiles by extra(mutableSetOf<String>(
"${docNames["examples"]}.tex",
"paper_init.tex",
"lecturedates.tex",
"doc_init.tex"
))
 
/***********************************************************************
* Images that are generated from another format.
* Key is the target image, value is the source image.
* All images will be mapped to imageDir (above).
*/
val generatedImages by extra(mutableMapOf<String, String>())
 
/***********************************************************************
* Files and directories to be removed by the clean task.
* Directories are separate because FileTree only stores files :(.
*/
val cleanFiles by extra(mutableSetOf<String>(
"*.aux",
"*.dvi",
"*.fdb_latexmk",
"*.fls",
"*.head",
"*.listing",
"*.log",
"*.nav",
"*.out",
"*.pyg",
"*.snm",
"*.synctex.gz",
"*.tmp",
"*.toc",
"*.vrb",
"*.xdv"
))
// These are regular expressions, NOT glob patterns.
val cleanDirs by extra(mutableSetOf<String>(
"""_minted.*""",
))
 
/***********************************************************************
* END OF CONFIGURABLE SETTINGS
***********************************************************************/
 
apply(from = file("lecture.gradle.kts"))
 
/***********************************************************************
* Supported targets (numbered documents).
* Automatically derived from docTargets.
*/
val numberedTargets by extra(docTargets.map { it + ".numbered" })
 
/***********************************************************************
* Standard document names (unnumbered).
* Automatically derived from docNames.
*/
val numberedNames by extra(docNames.mapValues { pair -> pair.value.replace("_", "_${lectureNum}_") })
 
/***********************************************************************
* Document dependencies.
* Automatically derived from slidesImages, slidesFiles, examplesImages,
* and examplesFiles. Images are mapped to imageDir.
* The slides, handout, and notes will always share the same dependencies.
* Only the examples will be different.
*/
val slideDependencies by extra(slidesImages.map { "${imageDir}/${it}" } + slidesFiles)
val docDependencies by extra(mapOf<String, List<String>>(
"slides" to slideDependencies,
"handout" to slideDependencies,
"notes" to slideDependencies,
"examples" to examplesImages.map { "${imageDir}/${it}" } + examplesFiles
))
 
/***********************************************************************
* LaTeX plugin configuration.
*/
plugins {
// https://plugins.gradle.org/plugin/org.danilopianini.gradle-latex
id("org.danilopianini.gradle-latex") version "0.2.7" // as of 2021-12-02
 
// for debugging task dependencies
// https://plugins.gradle.org/plugin/org.barfuin.gradle.taskinfo
// id("org.barfuin.gradle.taskinfo") version "1.3.1"
}
 
latex {
pdfLatexCommand.set("xelatex")
// Generate LaTeX build tasks for each of the targets.
docTargets.forEach { target ->
"${docNames[target]}" {
extraArguments = latexFlags
watching = docDependencies[target] as List<String>
}
}
}
 
// Add image task dependencies to the generated LaTeX tasks.
// No need to check whether there are any generated images, Gradle does
// the right thing if the list is empty.
tasks.matching { it.name.startsWith("pdfLatex") }.forEach {
it.dependsOn(tasks.matching { task -> task.name.startsWith("images.") })
}
 
/***********************************************************************
* Tasks.
*/
defaultTasks("docs", "numbered")
 
// All unnumbered documents.
tasks.register("docs") {
dependsOn(docTargets)
description = "Build all documents."
}
 
// Generate tasks for each of the unnumbered document targets.
(docTargets as Set<String>).forEach {
target -> tasks.register(target) {
dependsOn("pdfLatex.${docNames[target]}")
description = "Build ${target} document."
}
}
 
// All numbered documents.
tasks.register("numbered") {
dependsOn(numberedTargets)
description = "Create numbered versions of all documents."
}
 
/* Generate tasks for each of the numbered document targets.
* Slight oddity: numberedTargets is a List not a Set because it's
* generated by a map() call. It's not normally configurable, so OK.
*/
(numberedTargets as List<String>).forEach {
target -> tasks.register<Copy>(target) {
val plainTarget = target.replace(".numbered", "")
from(tasks.getByName("pdfLatex.${docNames[plainTarget]}"))
into(pdfDir)
// The LaTeX tasks include the .aux file in their outputs.
// Exclude or they will also be copied into the PDFs directory.
exclude("*.aux")
rename { src -> src.replace("_", "_${lectureNum}_") }
description = "Build numbered version of ${plainTarget} document."
}
}
 
// All generated images.
tasks.register("images") {
dependsOn(tasks.matching { it.name.startsWith("images.") })
description = "Generate images"
}
 
// Find folders matching regular expression.
// Adapted from <https://gist.github.com/Ethsaam/2b8dd748fcc25e6973bb>.
fun getListOfFolders(dir: Directory, pattern: String): Collection<File> {
return dir.getAsFile().listFiles({ file -> file.isDirectory() && file.name.matches(Regex(pattern)) }).toList()
}
 
// Remove files specified in cleanFiles and directories specified
// in cleanDirs.
tasks.register<Delete>("clean") {
description = "Clean up intermediate files."
delete(fileTree(layout.projectDirectory) { include(cleanFiles) })
cleanDirs.forEach { pattern ->
delete(getListOfFolders(layout.projectDirectory, pattern))
}
}
 
// Remove everything not under version control. This includes the .gradle
// directory.
tasks.register<Exec>("deepClean") {
description = "Remove all files not in Git."
commandLine("git", "clean", "-fXd")
}
 
/***********************************************************************
* Per-file tasks to convert images.
* By default these assume single-file inputs, e.g., foo.svg -> foo.pdf.
* If you have a file that is dependent on multiple inputs (e.g.,
* bar.pu -> foo.pu -> foo.pdf), register an independent task in
* lecture.gradle.kts by copying the corresponding registration code
* below, substituting the actual filenames in place of the variable
* references like key and generatedImages[key]. Also remove the file
* from generatedImages to avoid a name clash. You can access properties
* like imageDir via project.properties (these will probably need explicit
* type casting).
*/
 
// Generate per-file tasks to convert PlantUML to PDF.
generatedImages.filterValues {
it.matches(Regex(""".*\.(pu|puml|plantuml)$"""))
}.filterKeys {
it.matches(Regex(""".*\.pdf$"""))
}.keys
.forEach { key ->
val dst = imageDir.file(key)
val src = imageDir.file(generatedImages[key])
tasks.register<Exec>("images.${key}") {
// set working directory to imageDir so that relative includes work
workingDir(imageDir)
inputs.files(src)
outputs.files(dst)
// JAVA_TOOL_OPTIONS stops Java from stealing focus on macOS.
environment("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true")
// Direct PDF from PlantUML is problematic, go via SVG instead.
commandLine(
"bash",
"-c",
"plantuml -tsvg -pipe < ${src} | inkscape --pipe --export-area-drawing --export-text-to-path --export-type=pdf --export-filename=${dst}"
)
description = "Generate ${key} from ${generatedImages[key]}."
}
// Every PDF generation will have a different hash, so only run the
// task if the input is newer than the output.
tasks.getByPath("images.${key}").onlyIf { src.getAsFile().lastModified() > dst.getAsFile().lastModified() }
}
 
// Generate per-file tasks to convert SVG to PDF.
generatedImages.filterValues {
it.matches(Regex(""".*\.svg$"""))
}.filterKeys {
it.matches(Regex(""".*\.pdf$"""))
}.keys
.forEach { key ->
val dst = imageDir.file(key)
val src = imageDir.file(generatedImages[key])
tasks.register<Exec>("images.${key}") {
inputs.files(src)
outputs.files(dst)
commandLine(
"bash",
"-c",
"inkscape --export-area-drawing --export-text-to-path --export-type=pdf --export-filename=${dst} ${src}"
)
description = "Generate ${key} from ${generatedImages[key]}."
}
// Every PDF generation will have a different hash, so only run the
// task if the input is newer than the output.
tasks.getByPath("images.${key}").onlyIf { src.getAsFile().lastModified() > dst.getAsFile().lastModified() }
}
 
// Generate per-file tasks to convert R to PDF.
generatedImages.filterValues {
it.matches(Regex(""".*\.R$"""))
}.filterKeys {
it.matches(Regex(""".*\.pdf$"""))
}.keys
.forEach { key ->
val dst = imageDir.file(key)
val src = imageDir.file(generatedImages[key])
tasks.register<Exec>("images.${key}") {
val cropped = imageDir.file(key.replace(".pdf", "-crop.pdf"))
inputs.files(src)
outputs.files(dst)
commandLine(
"bash",
"-c",
"R --no-echo --file=${src} --args ${dst} && pdfcrop ${dst} ${cropped} && mv -f ${cropped} ${dst}"
)
description = "Generate ${key} from ${generatedImages[key]}."
}
// Every PDF generation will have a different hash, so only run the
// task if the input is newer than the output.
tasks.getByPath("images.${key}").onlyIf { src.getAsFile().lastModified() > dst.getAsFile().lastModified() }
}
 
// Debugging information.
tasks.register("debug") {
description = "Print debugging information."
doLast {
println("project name = <${project.name}>")
println("project directory = <${layout.projectDirectory}>")
// println("project.properties = <${project.properties}>")
println("docTargets = <${docTargets}>")
println("numberedTargets = <${numberedTargets}>")
println("lectureNum = <${lectureNum}>")
println("imageDir = <${imageDir}>")
println("docNames = <${docNames}>")
println("numberedNames = <${numberedNames}>")
println("slidesImages = <${slidesImages}>")
println("slidesFiles = <${slidesFiles}>")
println("examplesImages = <${examplesImages}>")
println("examplesFiles = <${examplesFiles}>")
println("docDependencies = <${docDependencies}>")
println("generatedImages = <${generatedImages}>")
println("latexFlags = <${latexFlags}>")
println("latex.terminalEmulator = <${project.latex.terminalEmulator.get()}>")
println("latex.waitTime = <${project.latex.waitTime.get()}>")
println("latex.waitUnit = <${project.latex.waitUnit.get()}>")
println("latex.pdfLatexCommand = <${project.latex.pdfLatexCommand.get()}>")
println("latex.bibTexCommand = <${project.latex.bibTexCommand.get()}>")
println("latex.inkscapeCommand = <${project.latex.inkscapeCommand.get()}>")
println("latex.gitLatexdiffCommand = <${project.latex.gitLatexdiffCommand.get()}>")
// tasks.matching { it.name.startsWith("pdfLatex") }.forEach {
// println("${it}, ${it.property("artifact")}")
// }
// tasks.forEach {
// println(it)
// println("inputs: ${it.getInputs().getFiles().getFiles()}")
// println("outputs: ${it.getOutputs().getFiles().getFiles()}")
// }
}
}
View
lecture.gradle.kts 0 → 100644
View
settings.gradle.kts 0 → 100644