package nz.stanger.lecture; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.danilopianini.gradle.latex.LatexExtension; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.Directory; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.Exec; import org.gradle.api.tasks.TaskContainer; /** * Gradle plugin to implement LaTeX lecture building infrastructure. */ public class Lecture implements Plugin<Project> { /** * Default group for lecture related tasks. */ public static final String LECTURE_TASK_GROUP = "lecture"; // Internal names for tasks. private static final String UNNUMBERED_TASK_NAME = "docs"; private static final String NUMBERED_TASK_NAME = "numbered"; private static final String IMAGES_TASK_NAME = "images"; private static final String CLEAN_TASK_NAME = "clean"; private static final String DEEP_CLEAN_TASK_NAME = "deepClean"; private static final String DEBUG_TASK_NAME = "debug"; private static final String PDFLATEX_TASK_NAME = "pdfLatex"; // Name of the lecture plugin extension. private static final String LECTURE_EXTENSION_NAME = "lecture"; // Names of document targets. private static final String CONTENT_TARGET_NAME = "content"; private static final String SLIDES_TARGET_NAME = "slides"; private static final String HANDOUT_TARGET_NAME = "handout"; private static final String NOTES_TARGET_NAME = "notes"; private static final String EXAMPLES_TARGET_NAME = "examples"; private static final String NUMBERED_TARGET_SUFFIX = "numbered"; // Prefix of generated lecture document files. private static final String LECTURE_PREFIX = "lecture"; // Only these target names can be used. private static final Set<String> VALID_TARGET_NAMES = Set.of( SLIDES_TARGET_NAME, HANDOUT_TARGET_NAME, NOTES_TARGET_NAME, EXAMPLES_TARGET_NAME ); // Standard directory names. private static final String IMAGES_DIRECTORY = "images"; private static final String PDFS_DIRECTORY = "pdfs"; // Some handy references that are used a lot. private TaskContainer projectTasks; private Directory projectDirectory; private LectureExtension lectureExtension; private LatexExtension latexExtension; /** * Find directories whose names match a regular expression. * * Adapted from {@link https://gist.github.com/Ethsaam/2b8dd748fcc25e6973bb}. * * @param dir the directory to start the search in * @param pattern regular expression to match directory names * @return the collection of matching directories */ private Collection<File> getListOfDirectories(Directory dir, String pattern) { return new ArrayList<>(Arrays.asList(dir.getAsFile().listFiles(file -> file.isDirectory() && file.getName().matches(pattern)))); } /** * Applies the plugin. * * @param project the project to apply the plugin to */ @Override public void apply(Project project) { projectTasks = project.getTasks(); projectDirectory = project.getLayout().getProjectDirectory(); project.getPluginManager().apply("org.danilopianini.gradle-latex"); // Plugin extensions. lectureExtension = project.getExtensions().create(LECTURE_EXTENSION_NAME, LectureExtension.class); latexExtension = project.getExtensions().getByType(LatexExtension.class); // Configure LaTeX extension. latexExtension.getPdfLatexCommand().set("xelatex"); /** * Set plugin defaults. * * Supported document targets. */ lectureExtension.getDocTargets().convention(VALID_TARGET_NAMES); // Validate document target configuration. Must be afterEvaluate() to // ensure everything has been finalised. project.afterEvaluate(p -> { lectureExtension.getDocTargets().get().stream().forEach(target -> { if (!VALID_TARGET_NAMES.contains(target)) { throw new RuntimeException("unsupported target name " + target); } }); }); // Supported targets (numbered documents). Automatically derived from // LectureExtension.docTargets. Provider<Set<String>> numberedDocTargets = lectureExtension.getDocTargets().map( docTargets -> docTargets.stream().map(target -> target + "." + NUMBERED_TARGET_SUFFIX).collect(Collectors.toSet()) ); // Standard lecture prefix string. lectureExtension.getDocPrefix().convention(LECTURE_PREFIX); lectureExtension.getLectureNum().convention(project.getName().substring(0, 2)); // Standard document names (unnumbered). Automatically derived from // LectureExtension.docTargets, plus "content". Provider<Map<String, String>> docNames = lectureExtension.getDocTargets().map(docTargets -> { Map<String, String> names = new HashMap<>(); docTargets.forEach(target -> names.put(target, lectureExtension.getDocPrefix().get() + "_" + target)); names.put(CONTENT_TARGET_NAME, lectureExtension.getDocPrefix().get() + "_" + CONTENT_TARGET_NAME); return names; }); // Standard document names (numbered). Automatically derived from // LectureExtension.docTargets, plus "content". Provider<Map<String, String>> numberedDocNames = lectureExtension.getDocTargets().map(docTargets -> { Map<String, String> names = new HashMap<>(); docTargets.forEach(target -> names.put(target, lectureExtension.getDocPrefix().get() + "_" + lectureExtension.getLectureNum().get() + "_" + target)); names.put(CONTENT_TARGET_NAME, lectureExtension.getDocPrefix().get() + "_" + lectureExtension.getLectureNum().get() + "_" + CONTENT_TARGET_NAME); return names; }); lectureExtension.getImageDir().convention(projectDirectory.dir(IMAGES_DIRECTORY)); lectureExtension.getPdfDir().convention(projectDirectory.dir(PDFS_DIRECTORY)); // slidesImages defaults to empty lectureExtension.getSlidesFiles().convention( lectureExtension.getDocPrefix().map( (String prefix) -> Set.of( prefix + "_" + CONTENT_TARGET_NAME + ".tex", "paper_init.tex", "lecturedates.tex", "doc_init.tex" ) ) ); // examplesImages defaults to empty lectureExtension.getExamplesFiles().convention( lectureExtension.getDocPrefix().map((String prefix) -> { if (lectureExtension.getDocTargets().get().contains(EXAMPLES_TARGET_NAME)) { return Set.of( prefix + "_" + EXAMPLES_TARGET_NAME + ".tex", "paper_init.tex", "lecturedates.tex", "doc_init.tex" ); } else { return Set.of(); } }) ); // 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. Provider<Set<String>> slideDependencies = lectureExtension.getSlidesImages().map(slideImages -> { Set<String> slideDeps = new HashSet<>(); slideDeps.addAll(slideImages.stream().map(image -> lectureExtension.getImageDir().get() + "/" + image).collect(Collectors.toSet())); slideDeps.addAll(lectureExtension.getSlidesFiles().get()); return slideDeps; }); Provider<Map<String, List<String>>> docDependencies = lectureExtension.getDocTargets().map(docTargets -> { Map<String, List<String>> docDeps = new HashMap<>(); docTargets.forEach(target -> { if (target.equals(EXAMPLES_TARGET_NAME)) { List<String> deps = lectureExtension.getExamplesFiles().get().stream().collect(Collectors.toList()); deps.addAll(lectureExtension.getExamplesImages().get().stream() .map(image -> lectureExtension.getImageDir().get() + "/" + image) .collect(Collectors.toList()) ); docDeps.put(target, deps); } else { docDeps.put(target, slideDependencies.get().stream().collect(Collectors.toList())); } }); return docDeps; }); lectureExtension.getCleanFiles().convention(Set.of( "*.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. lectureExtension.getCleanDirs().convention(Set.of( "_minted.*" )); lectureExtension.getLatexFlags().convention(Set.of( "-shell-escape", "-synctex=1", "-interaction=nonstopmode", "-halt-on-error", "-file-line-error" )); /** * Tasks. * * It doesn't always seem to be strictly necessary, but it seems safer * in general to assume that all tasks need to be afterEvaluate(). It's * certainly necessary for generated tasks that derive from * configuration variables. */ project.afterEvaluate(p -> { project.setDefaultTasks(Arrays.asList(UNNUMBERED_TASK_NAME, NUMBERED_TASK_NAME)); // All unnumbered documents. projectTasks.register(UNNUMBERED_TASK_NAME, task -> { task.setDescription("Build all documents (draft, unnumbered)."); task.setGroup(LECTURE_TASK_GROUP); task.dependsOn(lectureExtension.getDocTargets()); }); // Generate tasks for each of the unnumbered document targets. // All generated tasks that derive from configuration variables must be // afterEvaluate() to ensure everything has been finalised. lectureExtension.getDocTargets().get().forEach(target -> { projectTasks.register(target, task -> { task.setDescription("Build " + target + " document (draft, unnumbered)."); task.setGroup(LECTURE_TASK_GROUP); task.dependsOn(PDFLATEX_TASK_NAME + "." + docNames.get().get(target)); }); }); // All numbered documents. projectTasks.register(NUMBERED_TASK_NAME, task -> { task.setDescription("Build all documents (final, numbered)."); task.setGroup(LECTURE_TASK_GROUP); task.dependsOn(numberedDocTargets); }); // Generate tasks for each of the numbered document targets. numberedDocTargets.get().forEach(target -> { projectTasks.register(target, Copy.class, task -> { String baseTarget = target.replace("." + NUMBERED_TARGET_SUFFIX, ""); task.setDescription("Build " + baseTarget + " document (final, numbered)."); task.setGroup(LECTURE_TASK_GROUP); task.from(project.getTasksByName(PDFLATEX_TASK_NAME + "." + docNames.get().get(baseTarget), true)); task.into(lectureExtension.getPdfDir()); // The LaTeX tasks include the .aux file in their outputs. // Exclude or they will also be copied into the PDFs directory. task.exclude("*.aux"); task.rename(src -> src.replace("_", "_" + lectureExtension.getLectureNum().get() + "_")); }); }); // Generate LaTeX build tasks for each of the targets. This uses the // string invocation method supplied by the LaTeX plugin. lectureExtension.getDocTargets().get().forEach(target -> { latexExtension.invoke(docNames.get().get(target), (dsl) -> { dsl.setExtraArguments(lectureExtension.getLatexFlags().get()); dsl.setWatching(docDependencies.get().get(target)); return null; // workaround for Kotlin Unit (void) return }); }); // All generated images. Depends on any additional image generation // sub-tasks. projectTasks.register(IMAGES_TASK_NAME, task -> { task.setDescription("Generate images."); task.setGroup(LECTURE_TASK_GROUP); task.setDependsOn(projectTasks.withType(GenerateImageFromSourceTask.class)); }); // Ensure that the LaTeX tasks depend on the images task. This implies // that *all* LaTeX documents dependent on *all* generated images, // even when a document doesn't actually depend on a specific image. // This would be too much of a hassle to fix given the relatively // small benefit. projectTasks.matching(task -> task.getName().startsWith(PDFLATEX_TASK_NAME)).forEach(pdfLatexTask -> { pdfLatexTask.dependsOn(projectTasks.getByName(IMAGES_TASK_NAME)); }); // Remove files specified in cleanFiles and directories specified // in cleanDirs. projectTasks.register(CLEAN_TASK_NAME, Delete.class, task -> { task.setDescription("Clean up intermediate files."); task.setGroup(LECTURE_TASK_GROUP); ConfigurableFileTree tree = project.fileTree(projectDirectory); tree.include(lectureExtension.getCleanFiles().get()); task.delete(tree); lectureExtension.getCleanDirs().get().forEach( pattern -> task.delete(getListOfDirectories(projectDirectory, pattern)) ); }); // Remove everything not under version control. Unfortunately this *doesn't* // include the .gradle directory :(, but in hindsight that's not surprising. projectTasks.register(DEEP_CLEAN_TASK_NAME, Exec.class, task -> { task.setDescription("Remove all files not in Git."); task.setGroup(LECTURE_TASK_GROUP); task.setCommandLine("git", "clean", "-fXd"); }); }); // Debugging information. projectTasks.register(DEBUG_TASK_NAME, task -> { task.setDescription("Print debugging information."); task.setGroup(LECTURE_TASK_GROUP); task.doLast(s -> { System.out.println("project name = <" + project.getName() + ">"); System.out.println("project directory = <" + projectDirectory + ">"); System.out.println("docTargets = <" + lectureExtension.getDocTargets().get().toString() + ">"); System.out.println("numberedDocTargets = <" + numberedDocTargets.get().toString() + ">"); System.out.println("docPrefix = <" + lectureExtension.getDocPrefix().get() + ">"); System.out.println("lectureNum = <" + lectureExtension.getLectureNum().get() + ">"); System.out.println("docNames = <" + docNames.get().toString() + ">"); System.out.println("numberedDocNames = <" + numberedDocNames.get().toString() + ">"); System.out.println("imageDir = <" + lectureExtension.getImageDir().get().getAsFile().getName() + ">"); System.out.println("pdfDir = <" + lectureExtension.getPdfDir().get().getAsFile().getName() + ">"); System.out.println("slidesImages = <" + lectureExtension.getSlidesImages().get().toString() + ">"); System.out.println("slidesFiles = <" + lectureExtension.getSlidesFiles().get().toString() + ">"); System.out.println("examplesImages = <" + lectureExtension.getExamplesImages().get().toString() + ">"); System.out.println("examplesFiles = <" + lectureExtension.getExamplesFiles().get().toString() + ">"); System.out.println("slideDependencies = <" + slideDependencies.get().toString() + ">"); System.out.println("docDependencies = <" + docDependencies.get().toString() + ">"); System.out.println("cleanFiles = <" + lectureExtension.getCleanFiles().get().toString() + ">"); System.out.println("cleanDirs = <" + lectureExtension.getCleanDirs().get().toString() + ">"); System.out.println("latexFlags = <" + lectureExtension.getLatexFlags().get().toString() + ">"); System.out.println("latex.terminalEmulator = <" + latexExtension.getTerminalEmulator().get() + ">"); System.out.println("latex.waitTime = <" + latexExtension.getWaitTime().get() + ">"); System.out.println("latex.waitUnit = <" + latexExtension.getWaitUnit().get() + ">"); System.out.println("latex.pdfLatexCommand = <" + latexExtension.getPdfLatexCommand().get() + ">"); System.out.println("latex.bibTexCommand = <" + latexExtension.getBibTexCommand().get() + ">"); System.out.println("latex.inkscapeCommand = <" + latexExtension.getInkscapeCommand().get() + ">"); System.out.println("latex.gitLatexdiffCommand = <" + latexExtension.getGitLatexdiffCommand().get() + ">"); }); }); } }