/*********************************************************************** * CONFIGURABLE SETTINGS * * Override the following values in each lecture's build.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 the lecture's build.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 ***********************************************************************/ /*********************************************************************** * 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 { id("org.danilopianini.gradle-latex") // 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.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. Unfortunately this *doesn't* // include the .gradle directory :(, but in hindsight that's not surprising. 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 the * lecture's build.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].toString()) tasks.register<Exec>("images.${key}") { val cropped = imageDir.file(key.replace(".pdf", "-crop.pdf")) // set working directory to imageDir so that relative includes work workingDir(imageDir) inputs.files(src) outputs.files(dst) // Direct PDF from PlantUML is problematic, go via SVG instead. commandLine( "bash", "-c", "plantuml -tsvg -pipe < ${src} | rsvg-convert --format pdf > ${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() } } // 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].toString()) tasks.register<Exec>("images.${key}") { val cropped = imageDir.file(key.replace(".pdf", "-crop.pdf")) inputs.files(src) outputs.files(dst) commandLine( "bash", "-c", "rsvg-convert --format pdf ${src} > ${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() } } // 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].toString()) 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()}") // } } }