Waffle1412 revised this gist . Go to revision
1 file changed, 294 insertions, 283 deletions
compile_typst.sh
@@ -12,11 +12,11 @@ show_progress=1 | |||
12 | 12 | force=0 | |
13 | 13 | select_mode=0 | |
14 | 14 | use_cache=1 # Enable caching by default | |
15 | - | use_git=1 # Enable Git integration by default | |
16 | 15 | cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/typst_compiler" | |
17 | 16 | dependency_scan=1 # Enable dependency scanning by default | |
18 | 17 | compile_log="typst_compile_errors.log" | |
19 | 18 | move_log="typst_move_errors.log" | |
19 | + | use_mtime=1 # Use modification time instead of hashes by default | |
20 | 20 | declare -a exclude_patterns | |
21 | 21 | ||
22 | 22 | # Show usage/help information | |
@@ -38,9 +38,9 @@ Options: | |||
38 | 38 | -f, --force Force compilation even if PDF exists | |
39 | 39 | --no-progress Disable progress bar | |
40 | 40 | --no-cache Disable compilation caching | |
41 | - | --no-git Disable Git-based change detection | |
42 | 41 | --clear-cache Clear the cache before compiling | |
43 | 42 | --no-deps Disable dependency scanning | |
43 | + | --no-mtime Use file hashes instead of modification times | |
44 | 44 | --cache-dir DIR Custom cache directory (default: ~/.cache/typst_compiler) | |
45 | 45 | --compile-log FILE Custom location for compilation log (default: $compile_log) | |
46 | 46 | --move-log FILE Custom location for move log (default: $move_log) | |
@@ -52,7 +52,6 @@ Examples: | |||
52 | 52 | $(basename "$0") --verbose --skip-newer | |
53 | 53 | $(basename "$0") -S # Select files to compile interactively | |
54 | 54 | $(basename "$0") -e "**/test/**" -e "**/draft/**" | |
55 | - | $(basename "$0") --no-git # Disable Git-based change detection | |
56 | 55 | EOF | |
57 | 56 | } | |
58 | 57 | ||
@@ -103,10 +102,6 @@ while [[ $# -gt 0 ]]; do | |||
103 | 102 | use_cache=0 | |
104 | 103 | shift | |
105 | 104 | ;; | |
106 | - | --no-git) | |
107 | - | use_git=0 | |
108 | - | shift | |
109 | - | ;; | |
110 | 105 | --clear-cache) | |
111 | 106 | rm -rf "${cache_dir}" | |
112 | 107 | shift | |
@@ -115,6 +110,10 @@ while [[ $# -gt 0 ]]; do | |||
115 | 110 | dependency_scan=0 | |
116 | 111 | shift | |
117 | 112 | ;; | |
113 | + | --no-mtime) | |
114 | + | use_mtime=0 | |
115 | + | shift | |
116 | + | ;; | |
118 | 117 | --cache-dir) | |
119 | 118 | cache_dir="$2" | |
120 | 119 | shift 2 | |
@@ -161,7 +160,6 @@ check_tool() { | |||
161 | 160 | check_tool "fd" "cargo install fd-find or apt/brew install fd-find" | |
162 | 161 | check_tool "rg" "cargo install ripgrep or apt/brew install ripgrep" | |
163 | 162 | check_tool "parallel" "apt/brew install parallel" | |
164 | - | check_tool "sha256sum" "Built-in on most Linux systems, on macOS: brew install coreutils" | |
165 | 163 | ||
166 | 164 | # Check for skim and bat if select mode is enabled | |
167 | 165 | if [ "$select_mode" -eq 1 ]; then | |
@@ -191,7 +189,8 @@ ICON_INFO=" " | |||
191 | 189 | ICON_DEBUG=" " | |
192 | 190 | ICON_SELECT=" " | |
193 | 191 | ICON_CACHE=" " | |
194 | - | ICON_GIT=" " | |
192 | + | ICON_FILE=" " | |
193 | + | ICON_OPTIMIZE=" " | |
195 | 194 | ||
196 | 195 | # Logging functions | |
197 | 196 | log_debug() { | |
@@ -231,7 +230,7 @@ mkdir -p "$compile_failures_dir" "$move_failures_dir" "$progress_dir" | |||
231 | 230 | ||
232 | 231 | # Create cache directories if caching is enabled | |
233 | 232 | if [ "$use_cache" -eq 1 ]; then | |
234 | - | mkdir -p "${cache_dir}/hashes" "${cache_dir}/deps" "${cache_dir}/pdfs" | |
233 | + | mkdir -p "${cache_dir}/info" "${cache_dir}/deps" "${cache_dir}/pdfs" | |
235 | 234 | log_debug "Using cache directory: $cache_dir" | |
236 | 235 | fi | |
237 | 236 | ||
@@ -249,47 +248,6 @@ CWD=$(pwd) | |||
249 | 248 | ||
250 | 249 | log_info "Starting Typst compilation process..." | |
251 | 250 | ||
252 | - | # Check if we're in a Git repository | |
253 | - | in_git_repo=0 | |
254 | - | git_repo_root="" | |
255 | - | ||
256 | - | check_git_repo() { | |
257 | - | if [ "$use_git" -eq 1 ] && command -v git &> /dev/null; then | |
258 | - | # Check if we're in a git repo | |
259 | - | if git_repo_root=$(git rev-parse --show-toplevel 2>/dev/null); then | |
260 | - | in_git_repo=1 | |
261 | - | log_info "${ICON_GIT} Git repository detected at: ${BOLD}$git_repo_root${NC}" | |
262 | - | return 0 | |
263 | - | else | |
264 | - | log_debug "Not in a Git repository, falling back to file hashing" | |
265 | - | return 1 | |
266 | - | fi | |
267 | - | else | |
268 | - | log_debug "Git integration disabled or git not found" | |
269 | - | return 1 | |
270 | - | fi | |
271 | - | } | |
272 | - | ||
273 | - | # Get list of changed files from Git | |
274 | - | get_git_changed_files() { | |
275 | - | local git_files="$temp_dir/git_changed_files.txt" | |
276 | - | ||
277 | - | # Get list of staged, unstaged, and untracked files | |
278 | - | git -C "$git_repo_root" ls-files --modified --others --exclude-standard > "$git_files" | |
279 | - | # Add staged files that may not show as modified | |
280 | - | git -C "$git_repo_root" diff --name-only --cached >> "$git_files" | |
281 | - | ||
282 | - | # If no previous commit exists, consider all tracked files as changed | |
283 | - | if ! git -C "$git_repo_root" rev-parse HEAD &>/dev/null; then | |
284 | - | git -C "$git_repo_root" ls-files >> "$git_files" | |
285 | - | fi | |
286 | - | ||
287 | - | # Filter to keep only .typ files and remove duplicates | |
288 | - | sort -u "$git_files" | grep '\.typ$' > "$temp_dir/git_changed_typ_files.txt" | |
289 | - | ||
290 | - | log_debug "Git reports $(wc -l < "$temp_dir/git_changed_typ_files.txt") changed .typ files" | |
291 | - | } | |
292 | - | ||
293 | 251 | # Build exclude arguments for fd | |
294 | 252 | fd_exclude_args=() | |
295 | 253 | for pattern in "${exclude_patterns[@]}"; do | |
@@ -350,45 +308,14 @@ else | |||
350 | 308 | # Normal mode - process all files | |
351 | 309 | fd '\.typ$' --type f "${fd_exclude_args[@]}" . > "$typst_files_list" | |
352 | 310 | log_info "Found ${BOLD}$(wc -l < "$typst_files_list")${NC} Typst files to process" | |
353 | - | ||
354 | - | # Check for Git repository and get changed files | |
355 | - | check_git_repo | |
356 | - | if [ "$in_git_repo" -eq 1 ] && [ "$force" -eq 0 ]; then | |
357 | - | get_git_changed_files | |
358 | - | ||
359 | - | # If no forced compilation, filter the file list to only include changed files | |
360 | - | if [ -s "$temp_dir/git_changed_typ_files.txt" ]; then | |
361 | - | cp "$typst_files_list" "$temp_dir/all_typ_files.txt" | |
362 | - | ||
363 | - | # Convert Git changed files to absolute paths | |
364 | - | while read -r file; do | |
365 | - | echo "$git_repo_root/$file" | |
366 | - | done < "$temp_dir/git_changed_typ_files.txt" | sort > "$temp_dir/git_absolute_paths.txt" | |
367 | - | ||
368 | - | # Find the intersection of all files and Git changed files | |
369 | - | comm -12 <(sort "$typst_files_list") "$temp_dir/git_absolute_paths.txt" > "$temp_dir/files_to_compile.txt" | |
370 | - | ||
371 | - | # Use the filtered list if there are changes, otherwise keep using all files | |
372 | - | if [ -s "$temp_dir/files_to_compile.txt" ]; then | |
373 | - | cp "$temp_dir/files_to_compile.txt" "$typst_files_list" | |
374 | - | git_changes_count=$(wc -l < "$typst_files_list") | |
375 | - | log_info "${ICON_GIT} Processing ${BOLD}$git_changes_count${NC} files with Git changes" | |
376 | - | else | |
377 | - | log_info "${ICON_GIT} No Git changes detected for .typ files" | |
378 | - | ||
379 | - | # If no changes detected and not forced, update progress and exit early | |
380 | - | if [ "$force" -eq 0 ]; then | |
381 | - | log_success "No files need to be compiled." | |
382 | - | rm -rf "$temp_dir" | |
383 | - | exit 0 | |
384 | - | fi | |
385 | - | fi | |
386 | - | fi | |
387 | - | fi | |
388 | 311 | fi | |
389 | 312 | ||
390 | 313 | total_files=$(wc -l < "$typst_files_list") | |
391 | 314 | ||
315 | + | # Store file metadata for faster access | |
316 | + | declare -A file_mtime | |
317 | + | declare -A file_target_path | |
318 | + | ||
392 | 319 | # Function to extract Typst imports from a file | |
393 | 320 | extract_dependencies() { | |
394 | 321 | local file="$1" | |
@@ -415,44 +342,59 @@ extract_dependencies() { | |||
415 | 342 | } | |
416 | 343 | ||
417 | 344 | # Function to build dependency graph for all files | |
345 | + | # Returns dependencies in reverse order (dependents -> dependencies) | |
418 | 346 | build_dependency_graph() { | |
419 | 347 | log_info "${ICON_CACHE} Building dependency graph..." | |
420 | 348 | ||
421 | - | # Create a deps file for each Typst file | |
349 | + | # Create a deps file for each Typst file and build dependency map | |
350 | + | declare -A dependencies | |
351 | + | declare -A dependency_of | |
352 | + | ||
422 | 353 | while read -r file; do | |
423 | - | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
424 | - | deps_file="${cache_dir}/deps/${file_hash}.deps" | |
354 | + | file_id=$(echo "$file" | md5sum | cut -d' ' -f1) | |
355 | + | deps_file="${cache_dir}/deps/${file_id}.deps" | |
425 | 356 | ||
426 | 357 | # Skip if deps file is fresh and not forced | |
427 | 358 | if [ "$force" -eq 0 ] && [ -f "$deps_file" ] && [ "$file" -ot "$deps_file" ]; then | |
428 | 359 | log_debug "Using cached dependencies for $file" | |
429 | - | continue | |
430 | - | fi | |
431 | 360 | ||
432 | - | # Extract dependencies and save to deps file | |
433 | - | extract_dependencies "$file" > "$deps_file" | |
434 | - | log_debug "Updated dependencies for $file" | |
361 | + | # Load dependencies from cache | |
362 | + | dependencies["$file"]=$(cat "$deps_file" | tr '\n' ' ') | |
363 | + | ||
364 | + | # Update reverse dependency map | |
365 | + | while read -r dep; do | |
366 | + | dependency_of["$dep"]="${dependency_of["$dep"]} $file" | |
367 | + | done < "$deps_file" | |
368 | + | else | |
369 | + | # Extract dependencies and save to deps file | |
370 | + | extract_dependencies "$file" > "$deps_file" | |
371 | + | ||
372 | + | # Store dependencies in map | |
373 | + | dependencies["$file"]=$(cat "$deps_file" | tr '\n' ' ') | |
374 | + | ||
375 | + | # Update reverse dependency map | |
376 | + | while read -r dep; do | |
377 | + | dependency_of["$dep"]="${dependency_of["$dep"]} $file" | |
378 | + | done < "$deps_file" | |
379 | + | ||
380 | + | log_debug "Updated dependencies for $file" | |
381 | + | fi | |
435 | 382 | done < "$typst_files_list" | |
436 | 383 | ||
384 | + | # Store reverse dependencies back to cache | |
385 | + | for dep in "${!dependency_of[@]}"; do | |
386 | + | file_id=$(echo "$dep" | md5sum | cut -d' ' -f1) | |
387 | + | echo "${dependency_of["$dep"]}" > "${cache_dir}/deps/${file_id}.rev" | |
388 | + | done | |
389 | + | ||
437 | 390 | # Create sorted compilation order based on dependencies | |
438 | 391 | # Files with fewer dependencies (or no dependencies) come first | |
439 | - | if [ -f "$temp_dir/compile_order.txt" ]; then | |
440 | - | rm "$temp_dir/compile_order.txt" | |
441 | - | fi | |
392 | + | echo -n > "$temp_dir/compile_order.txt" | |
442 | 393 | ||
443 | 394 | # First pass: count dependencies for each file | |
444 | 395 | declare -A dep_counts | |
445 | 396 | while read -r file; do | |
446 | - | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
447 | - | deps_file="${cache_dir}/deps/${file_hash}.deps" | |
448 | - | ||
449 | - | # Count dependencies | |
450 | - | if [ -f "$deps_file" ]; then | |
451 | - | dep_count=$(wc -l < "$deps_file") | |
452 | - | else | |
453 | - | dep_count=0 | |
454 | - | fi | |
455 | - | ||
397 | + | dep_count=$(echo "${dependencies["$file"]}" | wc -w) | |
456 | 398 | dep_counts["$file"]=$dep_count | |
457 | 399 | done < "$typst_files_list" | |
458 | 400 | ||
@@ -462,136 +404,184 @@ build_dependency_graph() { | |||
462 | 404 | done < "$typst_files_list" | sort -n | cut -d' ' -f2- > "$temp_dir/compile_order.txt" | |
463 | 405 | ||
464 | 406 | log_debug "Created optimized compilation order" | |
407 | + | } | |
465 | 408 | ||
466 | - | # If in a Git repo, consider dependent files of changed files | |
467 | - | if [ "$in_git_repo" -eq 1 ] && [ -f "$temp_dir/git_changed_typ_files.txt" ]; then | |
468 | - | log_info "${ICON_GIT} Analyzing dependencies of changed files..." | |
469 | - | ||
470 | - | # Track files to be compiled (start with directly changed files) | |
471 | - | cp "$typst_files_list" "$temp_dir/files_to_compile_with_deps.txt" | |
472 | - | ||
473 | - | # Identify files that depend on changed files (reverse dependencies) | |
474 | - | while read -r file; do | |
475 | - | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
476 | - | ||
477 | - | # Find all files that have this file as a dependency | |
478 | - | grep -l "^$file$" "${cache_dir}"/deps/*.deps 2>/dev/null | while read -r dep_file; do | |
479 | - | # Extract the dependent file from the hash | |
480 | - | dependent_hash=$(basename "$dep_file" .deps) | |
481 | - | ||
482 | - | # Find the original filename for this hash | |
483 | - | while read -r potential_file; do | |
484 | - | potential_hash=$(echo "$potential_file" | md5sum | cut -d' ' -f1) | |
485 | - | if [ "$potential_hash" == "$dependent_hash" ]; then | |
486 | - | # Add this dependent file to the compilation list | |
487 | - | echo "$potential_file" >> "$temp_dir/files_to_compile_with_deps.txt" | |
488 | - | break | |
489 | - | fi | |
490 | - | done < "$temp_dir/all_typ_files.txt" | |
491 | - | done | |
492 | - | done < "$typst_files_list" | |
409 | + | # Function to get metadata of a typst file | |
410 | + | get_file_metadata() { | |
411 | + | local file="$1" | |
412 | + | local output_dir="$2" | |
493 | 413 | ||
494 | - | # Remove duplicates and update the files list | |
495 | - | sort -u "$temp_dir/files_to_compile_with_deps.txt" > "$typst_files_list" | |
414 | + | # Get file directory and basename | |
415 | + | local file_dir=$(dirname "$file") | |
416 | + | local filename=$(basename "$file") | |
417 | + | local basename="${filename%.typ}" | |
496 | 418 | ||
497 | - | # Update the total count | |
498 | - | new_total=$(wc -l < "$typst_files_list") | |
499 | - | if [ "$new_total" -gt "$total_files" ]; then | |
500 | - | log_info "${ICON_GIT} Added ${BOLD}$((new_total - total_files))${NC} additional files with dependencies" | |
501 | - | total_files=$new_total | |
502 | - | fi | |
419 | + | # Check if output directory exists or should be created | |
420 | + | local target_dir="$file_dir/$output_dir" | |
421 | + | ||
422 | + | # Store target paths | |
423 | + | file_target_path["$file"]="$target_dir/$basename.pdf" | |
424 | + | ||
425 | + | # Store modification time | |
426 | + | file_mtime["$file"]=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null) | |
427 | + | } | |
428 | + | ||
429 | + | # Function to initialize cache file for tracking modification times | |
430 | + | init_mtime_cache() { | |
431 | + | local mtime_cache="${cache_dir}/info/mtime_cache.txt" | |
432 | + | ||
433 | + | # Create empty cache file if it doesn't exist | |
434 | + | if [ ! -f "$mtime_cache" ]; then | |
435 | + | touch "$mtime_cache" | |
503 | 436 | fi | |
437 | + | ||
438 | + | # Load existing mtimes from cache | |
439 | + | declare -A cached_mtimes | |
440 | + | while IFS=' ' read -r file time; do | |
441 | + | if [ -n "$file" ] && [ -n "$time" ]; then | |
442 | + | cached_mtimes["$file"]="$time" | |
443 | + | fi | |
444 | + | done < "$mtime_cache" | |
445 | + | ||
446 | + | echo "$mtime_cache" | |
447 | + | ||
448 | + | # Return the associative array of cached mtimes | |
449 | + | for file in "${!cached_mtimes[@]}"; do | |
450 | + | echo "$file ${cached_mtimes["$file"]}" | |
451 | + | done | |
452 | + | } | |
453 | + | ||
454 | + | # Function to save the modification time cache | |
455 | + | save_mtime_cache() { | |
456 | + | local mtime_cache="${cache_dir}/info/mtime_cache.txt" | |
457 | + | > "$mtime_cache" | |
458 | + | ||
459 | + | # Save current file mtimes to cache | |
460 | + | for file in "${!file_mtime[@]}"; do | |
461 | + | echo "$file ${file_mtime["$file"]}" >> "$mtime_cache" | |
462 | + | done | |
504 | 463 | } | |
505 | 464 | ||
506 | - | # Function to check if a file has changed since last compilation | |
507 | - | has_file_changed() { | |
465 | + | # Function to determine if a file or its dependencies have changed | |
466 | + | needs_compilation() { | |
508 | 467 | local file="$1" | |
468 | + | local target_pdf="${file_target_path["$file"]}" | |
469 | + | local file_dir=$(dirname "$file") | |
470 | + | local basename=$(basename "${file%.typ}") | |
509 | 471 | ||
510 | - | # Force recompilation if requested | |
472 | + | # If force is enabled, always compile | |
511 | 473 | if [ "$force" -eq 1 ]; then | |
512 | - | log_debug "Force recompilation of $file" | |
513 | - | return 0 # File considered changed | |
474 | + | log_debug "Force compilation of $file" | |
475 | + | return 0 | |
514 | 476 | fi | |
515 | 477 | ||
516 | - | # If using Git and in a Git repo, check if file is in the changed list | |
517 | - | if [ "$in_git_repo" -eq 1 ] && [ "$use_git" -eq 1 ]; then | |
518 | - | # Convert to repo-relative path | |
519 | - | local repo_path="${file#$git_repo_root/}" | |
478 | + | # Check if the target path has been created | |
479 | + | if [ -z "$target_pdf" ]; then | |
480 | + | # Target not set, let's set it now | |
481 | + | get_file_metadata "$file" "$output_dir" | |
482 | + | target_pdf="${file_target_path["$file"]}" | |
483 | + | fi | |
520 | 484 | ||
521 | - | # Check if this file or any of its dependencies are changed according to Git | |
522 | - | if grep -q "^$git_repo_root/$repo_path$" "$temp_dir/git_absolute_paths.txt" 2>/dev/null; then | |
523 | - | log_debug "Git reports $file has changed" | |
524 | - | return 0 # File considered changed | |
485 | + | # Check if target directory exists | |
486 | + | target_dir=$(dirname "$target_pdf") | |
487 | + | if [ ! -d "$target_dir" ]; then | |
488 | + | if [ "$create_dirs" -eq 1 ]; then | |
489 | + | # Will create directory later | |
490 | + | log_debug "Will create directory $target_dir for $file" | |
491 | + | else | |
492 | + | # Skip if target directory doesn't exist and --create-dirs not specified | |
493 | + | log_debug "Skipping $file (no $output_dir directory)" | |
494 | + | return 1 | |
525 | 495 | fi | |
496 | + | fi | |
526 | 497 | ||
527 | - | # If not in the changed list and we're only processing Git changes, | |
528 | - | # then this file must have a dependency that changed | |
529 | - | if [ -f "$temp_dir/git_changed_typ_files.txt" ]; then | |
530 | - | # Check dependencies (if enabled) | |
531 | - | if [ "$dependency_scan" -eq 1 ]; then | |
532 | - | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
533 | - | local deps_file="${cache_dir}/deps/${file_hash}.deps" | |
534 | - | ||
535 | - | if [ -f "$deps_file" ]; then | |
536 | - | while read -r dep; do | |
537 | - | if has_file_changed "$dep"; then | |
538 | - | log_debug "Dependency $dep of $file has changed" | |
539 | - | return 0 # Dependency has changed | |
540 | - | fi | |
541 | - | done < "$deps_file" | |
542 | - | fi | |
543 | - | fi | |
498 | + | # Check if target PDF exists | |
499 | + | if [ ! -f "$target_pdf" ]; then | |
500 | + | log_debug "Target PDF doesn't exist for $file" | |
501 | + | return 0 | |
502 | + | fi | |
503 | + | ||
504 | + | # Check if PDF is newer than source and --skip-newer is specified | |
505 | + | if [ "$skip_newer" -eq 1 ]; then | |
506 | + | if [ -n "${file_mtime["$file"]}" ] && [ -f "$target_pdf" ]; then | |
507 | + | target_mtime=$(stat -c %Y "$target_pdf" 2>/dev/null || stat -f %m "$target_pdf" 2>/dev/null) | |
544 | 508 | ||
545 | - | log_debug "No changes to $file or its dependencies according to Git" | |
546 | - | return 1 # No changes detected via Git | |
509 | + | if [ "${file_mtime["$file"]}" -lt "$target_mtime" ]; then | |
510 | + | log_debug "Skipping $file (PDF is newer)" | |
511 | + | return 1 | |
512 | + | fi | |
547 | 513 | fi | |
548 | 514 | fi | |
549 | 515 | ||
550 | - | # Fall back to hash-based detection if Git not available or not conclusive | |
551 | - | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
552 | - | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
516 | + | # If not using mtime, do hash-based checks | |
517 | + | if [ "$use_mtime" -eq 0 ]; then | |
518 | + | # Use hash-based change detection | |
519 | + | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
520 | + | hash_file="${cache_dir}/info/${file_hash}.sha256" | |
553 | 521 | ||
554 | - | # Check if hash file exists | |
555 | - | if [ ! -f "$hash_file" ]; then | |
556 | - | log_debug "No previous hash for $file" | |
557 | - | return 0 # File considered changed | |
558 | - | fi | |
522 | + | if [ ! -f "$hash_file" ]; then | |
523 | + | log_debug "No previous hash for $file" | |
524 | + | return 0 | |
525 | + | fi | |
559 | 526 | ||
560 | - | # Compare current file hash with stored hash | |
561 | - | local current_hash=$(sha256sum "$file" | cut -d' ' -f1) | |
562 | - | local stored_hash=$(cat "$hash_file") | |
527 | + | current_hash=$(sha256sum "$file" | cut -d' ' -f1) | |
528 | + | stored_hash=$(cat "$hash_file") | |
563 | 529 | ||
564 | - | if [ "$current_hash" != "$stored_hash" ]; then | |
565 | - | log_debug "File $file has changed since last compilation" | |
566 | - | return 0 # File has changed | |
530 | + | if [ "$current_hash" != "$stored_hash" ]; then | |
531 | + | log_debug "File $file has changed since last compilation (hash)" | |
532 | + | return 0 | |
533 | + | fi | |
534 | + | else | |
535 | + | # Use mtime-based change detection | |
536 | + | if [ -n "$cached_mtimes" ]; then | |
537 | + | read_cached_mtimes=$(init_mtime_cache) | |
538 | + | while IFS=' ' read -r cached_file cached_time; do | |
539 | + | if [ "$cached_file" = "$file" ]; then | |
540 | + | current_mtime="${file_mtime["$file"]}" | |
541 | + | if [ "$current_mtime" != "$cached_time" ]; then | |
542 | + | log_debug "File $file has changed since last compilation (mtime)" | |
543 | + | return 0 | |
544 | + | fi | |
545 | + | break | |
546 | + | fi | |
547 | + | done <<< "$read_cached_mtimes" | |
548 | + | fi | |
567 | 549 | fi | |
568 | 550 | ||
569 | - | # Check if any dependencies have changed | |
551 | + | # Check if any dependencies have changed if dependency scanning is enabled | |
570 | 552 | if [ "$dependency_scan" -eq 1 ]; then | |
571 | - | local deps_file="${cache_dir}/deps/${file_hash}.deps" | |
553 | + | file_id=$(echo "$file" | md5sum | cut -d' ' -f1) | |
554 | + | deps_file="${cache_dir}/deps/${file_id}.deps" | |
555 | + | ||
572 | 556 | if [ -f "$deps_file" ]; then | |
573 | 557 | while read -r dep; do | |
574 | - | if has_file_changed "$dep"; then | |
575 | - | log_debug "Dependency $dep of $file has changed" | |
576 | - | return 0 # Dependency has changed | |
558 | + | # Recursively check if dependency has changed | |
559 | + | if needs_compilation "$dep"; then | |
560 | + | log_debug "Dependency $dep of $file needs compilation" | |
561 | + | return 0 | |
577 | 562 | fi | |
578 | 563 | done < "$deps_file" | |
579 | 564 | fi | |
580 | 565 | fi | |
581 | 566 | ||
582 | - | log_debug "File $file and its dependencies are unchanged" | |
583 | - | return 1 # File and dependencies haven't changed | |
567 | + | log_debug "File $file and its dependencies haven't changed" | |
568 | + | return 1 # No compilation needed | |
584 | 569 | } | |
585 | 570 | ||
586 | - | # Function to update file hash after compilation | |
587 | - | update_file_hash() { | |
571 | + | # Function to update file metadata after compilation | |
572 | + | update_file_metadata() { | |
588 | 573 | local file="$1" | |
589 | - | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
590 | - | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
591 | 574 | ||
592 | - | # Update hash file | |
593 | - | sha256sum "$file" | cut -d' ' -f1 > "$hash_file" | |
594 | - | log_debug "Updated hash for $file" | |
575 | + | # Update modification time | |
576 | + | file_mtime["$file"]=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null) | |
577 | + | ||
578 | + | # Update hash if not using mtime | |
579 | + | if [ "$use_mtime" -eq 0 ]; then | |
580 | + | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
581 | + | local hash_file="${cache_dir}/info/${file_hash}.sha256" | |
582 | + | sha256sum "$file" | cut -d' ' -f1 > "$hash_file" | |
583 | + | log_debug "Updated hash for $file" | |
584 | + | fi | |
595 | 585 | } | |
596 | 586 | ||
597 | 587 | # Function to cache compiled PDF | |
@@ -626,6 +616,12 @@ get_cached_pdf() { | |||
626 | 616 | return 1 | |
627 | 617 | fi | |
628 | 618 | ||
619 | + | # Create target directory if needed | |
620 | + | target_dir=$(dirname "$target_file") | |
621 | + | if [ ! -d "$target_dir" ] && [ "$create_dirs" -eq 1 ]; then | |
622 | + | mkdir -p "$target_dir" | |
623 | + | fi | |
624 | + | ||
629 | 625 | # Copy cached PDF to target location | |
630 | 626 | cp "$cached_pdf" "$target_file" | |
631 | 627 | log_debug "Retrieved cached PDF for $src_file" | |
@@ -644,9 +640,9 @@ update_progress_during() { | |||
644 | 640 | flock -n 200 || return 0 | |
645 | 641 | ||
646 | 642 | completed=$(find "$progress_dir" -type f | wc -l) | |
647 | - | percent=$((completed * 100 / total_files)) | |
643 | + | percent=$((completed * 100 / compilation_count)) | |
648 | 644 | bar_length=50 | |
649 | - | filled_length=$((bar_length * completed / total_files)) | |
645 | + | filled_length=$((bar_length * completed / compilation_count)) | |
650 | 646 | ||
651 | 647 | # Create the progress bar | |
652 | 648 | bar="" | |
@@ -667,10 +663,10 @@ update_progress_during() { | |||
667 | 663 | echo -ne "\r\033[K" | |
668 | 664 | echo -ne "${PURPLE}$ICON_WORKING Progress: ${GREEN}$bar ${BOLD}${percent}%${NC} " | |
669 | 665 | echo -ne "[${GREEN}${success}✓${NC}|${RED}${compile_fails}✗${NC}|${YELLOW}${move_fails}!${NC}] " | |
670 | - | echo -ne "${BLUE}($completed/$total_files)${NC}" | |
666 | + | echo -ne "${BLUE}($completed/$compilation_count)${NC}" | |
671 | 667 | ||
672 | 668 | # If all files are processed, print the completion message ONLY ONCE | |
673 | - | if [ $completed -eq $total_files ] && [ ! -e "$final_progress_file.done" ]; then | |
669 | + | if [ $completed -eq $compilation_count ] && [ ! -e "$final_progress_file.done" ]; then | |
674 | 670 | touch "$final_progress_file.done" | |
675 | 671 | echo -e "\n${GREEN}${BOLD}$ICON_COMPLETE All files processed!${NC}" | |
676 | 672 | fi | |
@@ -693,9 +689,9 @@ show_final_progress() { | |||
693 | 689 | fi | |
694 | 690 | ||
695 | 691 | completed=$(find "$progress_dir" -type f | wc -l) | |
696 | - | percent=$((completed * 100 / total_files)) | |
692 | + | percent=$((completed * 100 / compilation_count)) | |
697 | 693 | bar_length=50 | |
698 | - | filled_length=$((bar_length * completed / total_files)) | |
694 | + | filled_length=$((bar_length * completed / compilation_count)) | |
699 | 695 | ||
700 | 696 | # Create the progress bar | |
701 | 697 | bar="" | |
@@ -716,13 +712,13 @@ show_final_progress() { | |||
716 | 712 | echo -ne "\r\033[K" | |
717 | 713 | echo -ne "${PURPLE}$ICON_WORKING Progress: ${GREEN}$bar ${BOLD}${percent}%${NC} " | |
718 | 714 | echo -ne "[${GREEN}${success}✓${NC}|${RED}${compile_fails}✗${NC}|${YELLOW}${move_fails}!${NC}] " | |
719 | - | echo -ne "${BLUE}($completed/$total_files)${NC}" | |
715 | + | echo -ne "${BLUE}($completed/$compilation_count)${NC}" | |
720 | 716 | ||
721 | 717 | # Mark final progress as shown | |
722 | 718 | touch "$final_progress_file.done" | |
723 | 719 | ||
724 | 720 | # If all files are processed, print the completion message | |
725 | - | if [ $completed -eq $total_files ]; then | |
721 | + | if [ $completed -eq $compilation_count ]; then | |
726 | 722 | echo -e "\n${GREEN}${BOLD}$ICON_COMPLETE All files processed!${NC}" | |
727 | 723 | fi | |
728 | 724 | ) 200>"$progress_lock" | |
@@ -733,56 +729,10 @@ process_file() { | |||
733 | 729 | typfile="$1" | |
734 | 730 | file_id=$(echo "$typfile" | md5sum | cut -d' ' -f1) | |
735 | 731 | ||
736 | - | # Get the directory containing the .typ file | |
737 | - | typdir=$(dirname "$typfile") | |
738 | - | # Get the filename without path | |
739 | - | filename=$(basename "$typfile") | |
740 | - | # Get the filename without extension | |
741 | - | basename="${filename%.typ}" | |
742 | - | ||
743 | - | # Check if output directory exists or should be created | |
744 | - | target_dir="$typdir/$output_dir" | |
745 | - | if [ ! -d "$target_dir" ]; then | |
746 | - | if [ "$create_dirs" -eq 1 ]; then | |
747 | - | if [ "$dry_run" -eq 0 ]; then | |
748 | - | mkdir -p "$target_dir" | |
749 | - | log_debug "Created directory: $target_dir" | |
750 | - | else | |
751 | - | log_debug "[DRY RUN] Would create directory: $target_dir" | |
752 | - | fi | |
753 | - | else | |
754 | - | # Skip this file if output directory doesn't exist and --create-dirs not specified | |
755 | - | log_debug "Skipping $typfile (no $output_dir directory)" | |
756 | - | touch "$progress_dir/$file_id" | |
757 | - | update_progress_during | |
758 | - | return 0 | |
759 | - | fi | |
760 | - | fi | |
761 | - | ||
762 | - | target_pdf="$target_dir/$basename.pdf" | |
763 | - | ||
764 | - | # Skip if PDF is newer than source and --skip-newer is specified | |
765 | - | if [ "$skip_newer" -eq 1 ] && [ -f "$target_pdf" ]; then | |
766 | - | if [ "$typfile" -ot "$target_pdf" ] && [ "$force" -eq 0 ]; then | |
767 | - | log_debug "Skipping $typfile (PDF is newer)" | |
768 | - | touch "$progress_dir/$file_id" | |
769 | - | update_progress_during | |
770 | - | return 0 | |
771 | - | fi | |
772 | - | fi | |
773 | - | ||
774 | - | # Check if file has changed (if caching is enabled) | |
775 | - | if [ "$use_cache" -eq 1 ] && [ "$dry_run" -eq 0 ]; then | |
776 | - | if ! has_file_changed "$typfile"; then | |
777 | - | # Try to retrieve cached PDF | |
778 | - | if get_cached_pdf "$typfile" "$target_pdf"; then | |
779 | - | log_debug "Using cached PDF for $typfile" | |
780 | - | touch "$progress_dir/$file_id" | |
781 | - | update_progress_during | |
782 | - | return 0 | |
783 | - | fi | |
784 | - | fi | |
785 | - | fi | |
732 | + | # Get target path and other metadata | |
733 | + | target_pdf="${file_target_path["$typfile"]}" | |
734 | + | target_dir=$(dirname "$target_pdf") | |
735 | + | basename=$(basename "${typfile%.typ}") | |
786 | 736 | ||
787 | 737 | # Create a temporary file for capturing compiler output | |
788 | 738 | temp_output="$temp_dir/output_${file_id}.log" | |
@@ -802,7 +752,24 @@ process_file() { | |||
802 | 752 | return 0 | |
803 | 753 | fi | |
804 | 754 | ||
755 | + | # Ensure output directory exists if --create-dirs is enabled | |
756 | + | if [ "$create_dirs" -eq 1 ] && [ ! -d "$target_dir" ]; then | |
757 | + | mkdir -p "$target_dir" | |
758 | + | log_debug "Created directory: $target_dir" | |
759 | + | fi | |
760 | + | ||
761 | + | # Try to use cached PDF if available and file hasn't changed | |
762 | + | if [ "$use_cache" -eq 1 ]; then | |
763 | + | if get_cached_pdf "$typfile" "$target_pdf"; then | |
764 | + | log_debug "Used cached PDF for $typfile" | |
765 | + | touch "$progress_dir/$file_id" | |
766 | + | update_progress_during | |
767 | + | return 0 | |
768 | + | fi | |
769 | + | fi | |
770 | + | ||
805 | 771 | # Compile the .typ file using typst with --root flag and capture all output | |
772 | + | typdir=$(dirname "$typfile") | |
806 | 773 | if ! typst compile --root "$CWD" "$typfile" > "$temp_output.stdout" 2> "$temp_output.stderr"; then | |
807 | 774 | # Store the failure | |
808 | 775 | echo "$typfile" > "$compile_failures_dir/$file_id" | |
@@ -827,8 +794,8 @@ process_file() { | |||
827 | 794 | # Cache the PDF if caching is enabled | |
828 | 795 | if [ "$use_cache" -eq 1 ]; then | |
829 | 796 | cache_pdf "$typfile" "$temp_pdf" | |
830 | - | # Update file hash | |
831 | - | update_file_hash "$typfile" | |
797 | + | # Update file metadata | |
798 | + | update_file_metadata "$typfile" | |
832 | 799 | fi | |
833 | 800 | ||
834 | 801 | # Try to move the output PDF to the output directory | |
@@ -879,8 +846,7 @@ export -f log_info | |||
879 | 846 | export -f log_warning | |
880 | 847 | export -f log_error | |
881 | 848 | export -f log_success | |
882 | - | export -f has_file_changed | |
883 | - | export -f update_file_hash | |
849 | + | export -f update_file_metadata | |
884 | 850 | export -f cache_pdf | |
885 | 851 | export -f get_cached_pdf | |
886 | 852 | export CWD | |
@@ -893,11 +859,8 @@ export progress_lock | |||
893 | 859 | export compile_log | |
894 | 860 | export move_log | |
895 | 861 | export cache_dir | |
896 | - | export git_repo_root | |
897 | - | export in_git_repo | |
898 | - | export total_files | |
899 | 862 | export GREEN BLUE YELLOW RED CYAN PURPLE NC BOLD | |
900 | - | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG ICON_CACHE ICON_GIT | |
863 | + | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG ICON_CACHE ICON_FILE | |
901 | 864 | export verbose | |
902 | 865 | export quiet | |
903 | 866 | export output_dir | |
@@ -908,7 +871,7 @@ export show_progress | |||
908 | 871 | export force | |
909 | 872 | export use_cache | |
910 | 873 | export dependency_scan | |
911 | - | export use_git | |
874 | + | export use_mtime | |
912 | 875 | ||
913 | 876 | # Determine the number of CPU cores and use that many parallel jobs (if not specified) | |
914 | 877 | if [ "$jobs" -eq 0 ]; then | |
@@ -916,26 +879,73 @@ if [ "$jobs" -eq 0 ]; then | |||
916 | 879 | fi | |
917 | 880 | log_info "Using ${BOLD}$jobs${NC} parallel jobs for compilation" | |
918 | 881 | ||
919 | - | # Build dependency graph if dependency scanning is enabled | |
920 | - | if [ "$dependency_scan" -eq 1 ] && [ "$total_files" -gt 0 ]; then | |
921 | - | build_dependency_graph | |
922 | - | # Use optimized compilation order | |
923 | - | cp "$temp_dir/compile_order.txt" "$typst_files_list" | |
924 | - | log_info "Optimized compilation order based on dependencies" | |
925 | - | fi | |
882 | + | # Process file collection and get ready for compilation | |
883 | + | if [ "$total_files" -gt 0 ]; then | |
884 | + | # Get file metadata for all files | |
885 | + | log_info "${ICON_FILE} Collecting file information..." | |
886 | + | while read -r file; do | |
887 | + | get_file_metadata "$file" "$output_dir" | |
888 | + | done < "$typst_files_list" | |
926 | 889 | ||
927 | - | # Initialize progress bar if showing progress | |
928 | - | if [ "$show_progress" -eq 1 ]; then | |
929 | - | update_progress_during | |
930 | - | fi | |
890 | + | # Build dependency graph if dependency scanning is enabled | |
891 | + | if [ "$dependency_scan" -eq 1 ]; then | |
892 | + | build_dependency_graph | |
893 | + | # Use optimized compilation order | |
894 | + | cp "$temp_dir/compile_order.txt" "$typst_files_list" | |
895 | + | log_info "${ICON_OPTIMIZE} Optimized compilation order based on dependencies" | |
896 | + | fi | |
931 | 897 | ||
932 | - | # Process files in parallel with --will-cite to suppress citation notice | |
933 | - | if [ "$total_files" -gt 0 ]; then | |
934 | - | cat "$typst_files_list" | parallel --will-cite --jobs "$jobs" process_file | |
898 | + | # Filter files that need compilation | |
899 | + | log_info "${ICON_FILE} Checking for changes..." | |
900 | + | files_to_compile="$temp_dir/files_to_compile.txt" | |
901 | + | > "$files_to_compile" | |
902 | + | while read -r file; do | |
903 | + | if needs_compilation "$file"; then | |
904 | + | echo "$file" >> "$files_to_compile" | |
905 | + | else | |
906 | + | # Mark as processed but not compiled | |
907 | + | file_id=$(echo "$file" | md5sum | cut -d' ' -f1) | |
908 | + | touch "$progress_dir/$file_id" | |
909 | + | fi | |
910 | + | done < "$typst_files_list" | |
911 | + | ||
912 | + | # Count files that need compilation | |
913 | + | compilation_count=$(wc -l < "$files_to_compile") | |
914 | + | skipped_count=$((total_files - compilation_count)) | |
915 | + | ||
916 | + | # Export compilation count for progress tracking | |
917 | + | export compilation_count | |
918 | + | ||
919 | + | if [ "$compilation_count" -eq 0 ]; then | |
920 | + | log_success "All files are up to date, nothing to compile." | |
921 | + | rm -rf "$temp_dir" | |
922 | + | rm -f "$progress_lock" "$compile_log.lock" "$move_log.lock" | |
923 | + | ||
924 | + | if [ "$use_cache" -eq 1 ] && [ "$use_mtime" -eq 1 ]; then | |
925 | + | save_mtime_cache | |
926 | + | fi | |
927 | + | ||
928 | + | exit 0 | |
929 | + | fi | |
930 | + | ||
931 | + | log_info "Need to compile ${BOLD}$compilation_count${NC} files (skipping $skipped_count unchanged files)" | |
932 | + | ||
933 | + | # Initialize progress bar if showing progress | |
934 | + | if [ "$show_progress" -eq 1 ]; then | |
935 | + | update_progress_during | |
936 | + | fi | |
937 | + | ||
938 | + | # Process files in parallel with --will-cite to suppress citation notice | |
939 | + | cat "$files_to_compile" | parallel --will-cite --jobs "$jobs" process_file | |
935 | 940 | ||
936 | 941 | # Wait a moment for any remaining progress updates to complete | |
937 | 942 | sleep 0.5 | |
938 | 943 | ||
944 | + | # Save updated mtime cache | |
945 | + | if [ "$use_cache" -eq 1 ] && [ "$use_mtime" -eq 1 ]; then | |
946 | + | save_mtime_cache | |
947 | + | fi | |
948 | + | ||
939 | 949 | # Show the final progress exactly once if showing progress | |
940 | 950 | if [ "$show_progress" -eq 1 ]; then | |
941 | 951 | show_final_progress | |
@@ -978,6 +988,7 @@ if [ "$total_files" -gt 0 ]; then | |||
978 | 988 | echo -e " - Cached files: ${BOLD}$cache_files${NC}" | |
979 | 989 | echo -e " - Cache size: ${BOLD}$cache_size${NC}" | |
980 | 990 | echo -e " - Cache location: ${BOLD}$cache_dir${NC}" | |
991 | + | echo -e " - Change detection: ${BOLD}$([ "$use_mtime" -eq 1 ] && echo "Modification time" || echo "File hash")${NC}" | |
981 | 992 | fi | |
982 | 993 | else | |
983 | 994 | log_warning "No .typ files found to process." |
Waffle1412 revised this gist . Go to revision
No changes
Waffle1412 revised this gist . Go to revision
1 file changed, 165 insertions, 3 deletions
compile_typst.sh
@@ -12,6 +12,7 @@ show_progress=1 | |||
12 | 12 | force=0 | |
13 | 13 | select_mode=0 | |
14 | 14 | use_cache=1 # Enable caching by default | |
15 | + | use_git=1 # Enable Git integration by default | |
15 | 16 | cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/typst_compiler" | |
16 | 17 | dependency_scan=1 # Enable dependency scanning by default | |
17 | 18 | compile_log="typst_compile_errors.log" | |
@@ -37,6 +38,7 @@ Options: | |||
37 | 38 | -f, --force Force compilation even if PDF exists | |
38 | 39 | --no-progress Disable progress bar | |
39 | 40 | --no-cache Disable compilation caching | |
41 | + | --no-git Disable Git-based change detection | |
40 | 42 | --clear-cache Clear the cache before compiling | |
41 | 43 | --no-deps Disable dependency scanning | |
42 | 44 | --cache-dir DIR Custom cache directory (default: ~/.cache/typst_compiler) | |
@@ -50,6 +52,7 @@ Examples: | |||
50 | 52 | $(basename "$0") --verbose --skip-newer | |
51 | 53 | $(basename "$0") -S # Select files to compile interactively | |
52 | 54 | $(basename "$0") -e "**/test/**" -e "**/draft/**" | |
55 | + | $(basename "$0") --no-git # Disable Git-based change detection | |
53 | 56 | EOF | |
54 | 57 | } | |
55 | 58 | ||
@@ -100,6 +103,10 @@ while [[ $# -gt 0 ]]; do | |||
100 | 103 | use_cache=0 | |
101 | 104 | shift | |
102 | 105 | ;; | |
106 | + | --no-git) | |
107 | + | use_git=0 | |
108 | + | shift | |
109 | + | ;; | |
103 | 110 | --clear-cache) | |
104 | 111 | rm -rf "${cache_dir}" | |
105 | 112 | shift | |
@@ -184,6 +191,7 @@ ICON_INFO=" " | |||
184 | 191 | ICON_DEBUG=" " | |
185 | 192 | ICON_SELECT=" " | |
186 | 193 | ICON_CACHE=" " | |
194 | + | ICON_GIT=" " | |
187 | 195 | ||
188 | 196 | # Logging functions | |
189 | 197 | log_debug() { | |
@@ -241,6 +249,47 @@ CWD=$(pwd) | |||
241 | 249 | ||
242 | 250 | log_info "Starting Typst compilation process..." | |
243 | 251 | ||
252 | + | # Check if we're in a Git repository | |
253 | + | in_git_repo=0 | |
254 | + | git_repo_root="" | |
255 | + | ||
256 | + | check_git_repo() { | |
257 | + | if [ "$use_git" -eq 1 ] && command -v git &> /dev/null; then | |
258 | + | # Check if we're in a git repo | |
259 | + | if git_repo_root=$(git rev-parse --show-toplevel 2>/dev/null); then | |
260 | + | in_git_repo=1 | |
261 | + | log_info "${ICON_GIT} Git repository detected at: ${BOLD}$git_repo_root${NC}" | |
262 | + | return 0 | |
263 | + | else | |
264 | + | log_debug "Not in a Git repository, falling back to file hashing" | |
265 | + | return 1 | |
266 | + | fi | |
267 | + | else | |
268 | + | log_debug "Git integration disabled or git not found" | |
269 | + | return 1 | |
270 | + | fi | |
271 | + | } | |
272 | + | ||
273 | + | # Get list of changed files from Git | |
274 | + | get_git_changed_files() { | |
275 | + | local git_files="$temp_dir/git_changed_files.txt" | |
276 | + | ||
277 | + | # Get list of staged, unstaged, and untracked files | |
278 | + | git -C "$git_repo_root" ls-files --modified --others --exclude-standard > "$git_files" | |
279 | + | # Add staged files that may not show as modified | |
280 | + | git -C "$git_repo_root" diff --name-only --cached >> "$git_files" | |
281 | + | ||
282 | + | # If no previous commit exists, consider all tracked files as changed | |
283 | + | if ! git -C "$git_repo_root" rev-parse HEAD &>/dev/null; then | |
284 | + | git -C "$git_repo_root" ls-files >> "$git_files" | |
285 | + | fi | |
286 | + | ||
287 | + | # Filter to keep only .typ files and remove duplicates | |
288 | + | sort -u "$git_files" | grep '\.typ$' > "$temp_dir/git_changed_typ_files.txt" | |
289 | + | ||
290 | + | log_debug "Git reports $(wc -l < "$temp_dir/git_changed_typ_files.txt") changed .typ files" | |
291 | + | } | |
292 | + | ||
244 | 293 | # Build exclude arguments for fd | |
245 | 294 | fd_exclude_args=() | |
246 | 295 | for pattern in "${exclude_patterns[@]}"; do | |
@@ -301,6 +350,41 @@ else | |||
301 | 350 | # Normal mode - process all files | |
302 | 351 | fd '\.typ$' --type f "${fd_exclude_args[@]}" . > "$typst_files_list" | |
303 | 352 | log_info "Found ${BOLD}$(wc -l < "$typst_files_list")${NC} Typst files to process" | |
353 | + | ||
354 | + | # Check for Git repository and get changed files | |
355 | + | check_git_repo | |
356 | + | if [ "$in_git_repo" -eq 1 ] && [ "$force" -eq 0 ]; then | |
357 | + | get_git_changed_files | |
358 | + | ||
359 | + | # If no forced compilation, filter the file list to only include changed files | |
360 | + | if [ -s "$temp_dir/git_changed_typ_files.txt" ]; then | |
361 | + | cp "$typst_files_list" "$temp_dir/all_typ_files.txt" | |
362 | + | ||
363 | + | # Convert Git changed files to absolute paths | |
364 | + | while read -r file; do | |
365 | + | echo "$git_repo_root/$file" | |
366 | + | done < "$temp_dir/git_changed_typ_files.txt" | sort > "$temp_dir/git_absolute_paths.txt" | |
367 | + | ||
368 | + | # Find the intersection of all files and Git changed files | |
369 | + | comm -12 <(sort "$typst_files_list") "$temp_dir/git_absolute_paths.txt" > "$temp_dir/files_to_compile.txt" | |
370 | + | ||
371 | + | # Use the filtered list if there are changes, otherwise keep using all files | |
372 | + | if [ -s "$temp_dir/files_to_compile.txt" ]; then | |
373 | + | cp "$temp_dir/files_to_compile.txt" "$typst_files_list" | |
374 | + | git_changes_count=$(wc -l < "$typst_files_list") | |
375 | + | log_info "${ICON_GIT} Processing ${BOLD}$git_changes_count${NC} files with Git changes" | |
376 | + | else | |
377 | + | log_info "${ICON_GIT} No Git changes detected for .typ files" | |
378 | + | ||
379 | + | # If no changes detected and not forced, update progress and exit early | |
380 | + | if [ "$force" -eq 0 ]; then | |
381 | + | log_success "No files need to be compiled." | |
382 | + | rm -rf "$temp_dir" | |
383 | + | exit 0 | |
384 | + | fi | |
385 | + | fi | |
386 | + | fi | |
387 | + | fi | |
304 | 388 | fi | |
305 | 389 | ||
306 | 390 | total_files=$(wc -l < "$typst_files_list") | |
@@ -378,13 +462,50 @@ build_dependency_graph() { | |||
378 | 462 | done < "$typst_files_list" | sort -n | cut -d' ' -f2- > "$temp_dir/compile_order.txt" | |
379 | 463 | ||
380 | 464 | log_debug "Created optimized compilation order" | |
465 | + | ||
466 | + | # If in a Git repo, consider dependent files of changed files | |
467 | + | if [ "$in_git_repo" -eq 1 ] && [ -f "$temp_dir/git_changed_typ_files.txt" ]; then | |
468 | + | log_info "${ICON_GIT} Analyzing dependencies of changed files..." | |
469 | + | ||
470 | + | # Track files to be compiled (start with directly changed files) | |
471 | + | cp "$typst_files_list" "$temp_dir/files_to_compile_with_deps.txt" | |
472 | + | ||
473 | + | # Identify files that depend on changed files (reverse dependencies) | |
474 | + | while read -r file; do | |
475 | + | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
476 | + | ||
477 | + | # Find all files that have this file as a dependency | |
478 | + | grep -l "^$file$" "${cache_dir}"/deps/*.deps 2>/dev/null | while read -r dep_file; do | |
479 | + | # Extract the dependent file from the hash | |
480 | + | dependent_hash=$(basename "$dep_file" .deps) | |
481 | + | ||
482 | + | # Find the original filename for this hash | |
483 | + | while read -r potential_file; do | |
484 | + | potential_hash=$(echo "$potential_file" | md5sum | cut -d' ' -f1) | |
485 | + | if [ "$potential_hash" == "$dependent_hash" ]; then | |
486 | + | # Add this dependent file to the compilation list | |
487 | + | echo "$potential_file" >> "$temp_dir/files_to_compile_with_deps.txt" | |
488 | + | break | |
489 | + | fi | |
490 | + | done < "$temp_dir/all_typ_files.txt" | |
491 | + | done | |
492 | + | done < "$typst_files_list" | |
493 | + | ||
494 | + | # Remove duplicates and update the files list | |
495 | + | sort -u "$temp_dir/files_to_compile_with_deps.txt" > "$typst_files_list" | |
496 | + | ||
497 | + | # Update the total count | |
498 | + | new_total=$(wc -l < "$typst_files_list") | |
499 | + | if [ "$new_total" -gt "$total_files" ]; then | |
500 | + | log_info "${ICON_GIT} Added ${BOLD}$((new_total - total_files))${NC} additional files with dependencies" | |
501 | + | total_files=$new_total | |
502 | + | fi | |
503 | + | fi | |
381 | 504 | } | |
382 | 505 | ||
383 | 506 | # Function to check if a file has changed since last compilation | |
384 | 507 | has_file_changed() { | |
385 | 508 | local file="$1" | |
386 | - | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
387 | - | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
388 | 509 | ||
389 | 510 | # Force recompilation if requested | |
390 | 511 | if [ "$force" -eq 1 ]; then | |
@@ -392,6 +513,44 @@ has_file_changed() { | |||
392 | 513 | return 0 # File considered changed | |
393 | 514 | fi | |
394 | 515 | ||
516 | + | # If using Git and in a Git repo, check if file is in the changed list | |
517 | + | if [ "$in_git_repo" -eq 1 ] && [ "$use_git" -eq 1 ]; then | |
518 | + | # Convert to repo-relative path | |
519 | + | local repo_path="${file#$git_repo_root/}" | |
520 | + | ||
521 | + | # Check if this file or any of its dependencies are changed according to Git | |
522 | + | if grep -q "^$git_repo_root/$repo_path$" "$temp_dir/git_absolute_paths.txt" 2>/dev/null; then | |
523 | + | log_debug "Git reports $file has changed" | |
524 | + | return 0 # File considered changed | |
525 | + | fi | |
526 | + | ||
527 | + | # If not in the changed list and we're only processing Git changes, | |
528 | + | # then this file must have a dependency that changed | |
529 | + | if [ -f "$temp_dir/git_changed_typ_files.txt" ]; then | |
530 | + | # Check dependencies (if enabled) | |
531 | + | if [ "$dependency_scan" -eq 1 ]; then | |
532 | + | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
533 | + | local deps_file="${cache_dir}/deps/${file_hash}.deps" | |
534 | + | ||
535 | + | if [ -f "$deps_file" ]; then | |
536 | + | while read -r dep; do | |
537 | + | if has_file_changed "$dep"; then | |
538 | + | log_debug "Dependency $dep of $file has changed" | |
539 | + | return 0 # Dependency has changed | |
540 | + | fi | |
541 | + | done < "$deps_file" | |
542 | + | fi | |
543 | + | fi | |
544 | + | ||
545 | + | log_debug "No changes to $file or its dependencies according to Git" | |
546 | + | return 1 # No changes detected via Git | |
547 | + | fi | |
548 | + | fi | |
549 | + | ||
550 | + | # Fall back to hash-based detection if Git not available or not conclusive | |
551 | + | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
552 | + | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
553 | + | ||
395 | 554 | # Check if hash file exists | |
396 | 555 | if [ ! -f "$hash_file" ]; then | |
397 | 556 | log_debug "No previous hash for $file" | |
@@ -734,9 +893,11 @@ export progress_lock | |||
734 | 893 | export compile_log | |
735 | 894 | export move_log | |
736 | 895 | export cache_dir | |
896 | + | export git_repo_root | |
897 | + | export in_git_repo | |
737 | 898 | export total_files | |
738 | 899 | export GREEN BLUE YELLOW RED CYAN PURPLE NC BOLD | |
739 | - | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG ICON_CACHE | |
900 | + | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG ICON_CACHE ICON_GIT | |
740 | 901 | export verbose | |
741 | 902 | export quiet | |
742 | 903 | export output_dir | |
@@ -747,6 +908,7 @@ export show_progress | |||
747 | 908 | export force | |
748 | 909 | export use_cache | |
749 | 910 | export dependency_scan | |
911 | + | export use_git | |
750 | 912 | ||
751 | 913 | # Determine the number of CPU cores and use that many parallel jobs (if not specified) | |
752 | 914 | if [ "$jobs" -eq 0 ]; then |
Waffle1412 revised this gist . Go to revision
1 file changed, 257 insertions, 106 deletions
compile_typst.sh
@@ -11,6 +11,9 @@ skip_newer=0 | |||
11 | 11 | show_progress=1 | |
12 | 12 | force=0 | |
13 | 13 | select_mode=0 | |
14 | + | use_cache=1 # Enable caching by default | |
15 | + | cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/typst_compiler" | |
16 | + | dependency_scan=1 # Enable dependency scanning by default | |
14 | 17 | compile_log="typst_compile_errors.log" | |
15 | 18 | move_log="typst_move_errors.log" | |
16 | 19 | declare -a exclude_patterns | |
@@ -33,6 +36,10 @@ Options: | |||
33 | 36 | -S, --select Interactive selection mode using skim | |
34 | 37 | -f, --force Force compilation even if PDF exists | |
35 | 38 | --no-progress Disable progress bar | |
39 | + | --no-cache Disable compilation caching | |
40 | + | --clear-cache Clear the cache before compiling | |
41 | + | --no-deps Disable dependency scanning | |
42 | + | --cache-dir DIR Custom cache directory (default: ~/.cache/typst_compiler) | |
36 | 43 | --compile-log FILE Custom location for compilation log (default: $compile_log) | |
37 | 44 | --move-log FILE Custom location for move log (default: $move_log) | |
38 | 45 | -e, --exclude PATTERN Exclude files matching pattern (can be used multiple times) | |
@@ -89,6 +96,22 @@ while [[ $# -gt 0 ]]; do | |||
89 | 96 | show_progress=0 | |
90 | 97 | shift | |
91 | 98 | ;; | |
99 | + | --no-cache) | |
100 | + | use_cache=0 | |
101 | + | shift | |
102 | + | ;; | |
103 | + | --clear-cache) | |
104 | + | rm -rf "${cache_dir}" | |
105 | + | shift | |
106 | + | ;; | |
107 | + | --no-deps) | |
108 | + | dependency_scan=0 | |
109 | + | shift | |
110 | + | ;; | |
111 | + | --cache-dir) | |
112 | + | cache_dir="$2" | |
113 | + | shift 2 | |
114 | + | ;; | |
92 | 115 | --compile-log) | |
93 | 116 | compile_log="$2" | |
94 | 117 | shift 2 | |
@@ -131,6 +154,7 @@ check_tool() { | |||
131 | 154 | check_tool "fd" "cargo install fd-find or apt/brew install fd-find" | |
132 | 155 | check_tool "rg" "cargo install ripgrep or apt/brew install ripgrep" | |
133 | 156 | check_tool "parallel" "apt/brew install parallel" | |
157 | + | check_tool "sha256sum" "Built-in on most Linux systems, on macOS: brew install coreutils" | |
134 | 158 | ||
135 | 159 | # Check for skim and bat if select mode is enabled | |
136 | 160 | if [ "$select_mode" -eq 1 ]; then | |
@@ -159,6 +183,7 @@ ICON_SUMMARY=" " | |||
159 | 183 | ICON_INFO=" " | |
160 | 184 | ICON_DEBUG=" " | |
161 | 185 | ICON_SELECT=" " | |
186 | + | ICON_CACHE=" " | |
162 | 187 | ||
163 | 188 | # Logging functions | |
164 | 189 | log_debug() { | |
@@ -196,6 +221,12 @@ move_failures_dir="$temp_dir/move_failures" | |||
196 | 221 | progress_dir="$temp_dir/progress" | |
197 | 222 | mkdir -p "$compile_failures_dir" "$move_failures_dir" "$progress_dir" | |
198 | 223 | ||
224 | + | # Create cache directories if caching is enabled | |
225 | + | if [ "$use_cache" -eq 1 ]; then | |
226 | + | mkdir -p "${cache_dir}/hashes" "${cache_dir}/deps" "${cache_dir}/pdfs" | |
227 | + | log_debug "Using cache directory: $cache_dir" | |
228 | + | fi | |
229 | + | ||
199 | 230 | # Create a lock file for progress updates and a flag for final progress | |
200 | 231 | progress_lock="/tmp/typst_progress_lock" | |
201 | 232 | final_progress_file="$temp_dir/final_progress" | |
@@ -219,92 +250,13 @@ done | |||
219 | 250 | # Create a list of files to process | |
220 | 251 | typst_files_list="$temp_dir/typst_files.txt" | |
221 | 252 | ||
222 | - | # Create a custom bat configuration for Typst syntax | |
223 | - | setup_bat_for_typst() { | |
224 | - | bat_config_dir="$temp_dir/bat_config" | |
225 | - | mkdir -p "$bat_config_dir/syntaxes" | |
226 | - | ||
227 | - | # Create a basic syntax mapping file for Typst | |
228 | - | cat > "$bat_config_dir/syntaxes/typst.sublime-syntax" << 'TYPST_SYNTAX' | |
229 | - | %YAML 1.2 | |
230 | - | --- | |
231 | - | name: Typst | |
232 | - | file_extensions: | |
233 | - | - typ | |
234 | - | scope: source.typst | |
235 | - | ||
236 | - | contexts: | |
237 | - | main: | |
238 | - | # Comments | |
239 | - | - match: /\* | |
240 | - | scope: comment.block.typst | |
241 | - | push: block_comment | |
242 | - | ||
243 | - | - match: // | |
244 | - | scope: comment.line.double-slash.typst | |
245 | - | push: line_comment | |
246 | - | ||
247 | - | # Strings | |
248 | - | - match: '"' | |
249 | - | scope: punctuation.definition.string.begin.typst | |
250 | - | push: double_string | |
251 | - | ||
252 | - | # Math | |
253 | - | - match: '\$' | |
254 | - | scope: punctuation.definition.math.begin.typst | |
255 | - | push: math | |
256 | - | ||
257 | - | # Functions | |
258 | - | - match: '#([a-zA-Z][a-zA-Z0-9_]*)' | |
259 | - | scope: entity.name.function.typst | |
260 | - | ||
261 | - | # Variables | |
262 | - | - match: '\b([a-zA-Z][a-zA-Z0-9_]*)\s*:' | |
263 | - | scope: variable.other.typst | |
264 | - | ||
265 | - | # Keywords | |
266 | - | - match: '\b(let|set|show|if|else|for|in|while|return|import|include|at|do|not|and|or|none|auto)\b' | |
267 | - | scope: keyword.control.typst | |
268 | - | ||
269 | - | block_comment: | |
270 | - | - match: \*/ | |
271 | - | scope: comment.block.typst | |
272 | - | pop: true | |
273 | - | - match: . | |
274 | - | scope: comment.block.typst | |
275 | - | ||
276 | - | line_comment: | |
277 | - | - match: $ | |
278 | - | pop: true | |
279 | - | - match: . | |
280 | - | scope: comment.line.double-slash.typst | |
281 | - | ||
282 | - | double_string: | |
283 | - | - match: '"' | |
284 | - | scope: punctuation.definition.string.end.typst | |
285 | - | pop: true | |
286 | - | - match: \\. | |
287 | - | scope: constant.character.escape.typst | |
288 | - | - match: . | |
289 | - | scope: string.quoted.double.typst | |
290 | - | ||
291 | - | math: | |
292 | - | - match: '\$' | |
293 | - | scope: punctuation.definition.math.end.typst | |
294 | - | pop: true | |
295 | - | - match: . | |
296 | - | scope: markup.math.typst | |
297 | - | TYPST_SYNTAX | |
298 | - | ||
299 | - | echo "$bat_config_dir" | |
300 | - | } | |
301 | - | ||
302 | 253 | if [ "$select_mode" -eq 1 ]; then | |
303 | 254 | log_info "${PURPLE}$ICON_SELECT${NC} Interactive selection mode enabled" | |
304 | 255 | log_info "Use TAB to select multiple files, ENTER to confirm" | |
305 | 256 | ||
306 | 257 | # Set up bat configuration for Typst syntax highlighting | |
307 | - | bat_config=$(setup_bat_for_typst) | |
258 | + | bat_config="$temp_dir/bat_config" | |
259 | + | mkdir -p "$bat_config/syntaxes" | |
308 | 260 | export BAT_CONFIG_PATH="$bat_config" | |
309 | 261 | ||
310 | 262 | # Use skim with bat for preview to select files | |
@@ -345,23 +297,6 @@ if [ "$select_mode" -eq 1 ]; then | |||
345 | 297 | total_available=$(wc -l < "$typst_files_list.all") | |
346 | 298 | ||
347 | 299 | log_info "Selected ${BOLD}$total_selected${NC} out of ${BOLD}$total_available${NC} available files" | |
348 | - | ||
349 | - | # Display tip about better Typst syntax highlighting | |
350 | - | cat << 'TYPST_TIP' | |
351 | - | ||
352 | - | 📝 To get proper Typst syntax highlighting in bat: | |
353 | - | ||
354 | - | 1. Create a custom Typst syntax file: | |
355 | - | mkdir -p ~/.config/bat/syntaxes | |
356 | - | curl -L https://raw.githubusercontent.com/typst/typst-vs-code/main/syntaxes/typst.tmLanguage.json \ | |
357 | - | -o ~/.config/bat/syntaxes/typst.tmLanguage.json | |
358 | - | ||
359 | - | 2. Build bat's syntax cache: | |
360 | - | bat cache --build | |
361 | - | ||
362 | - | This will provide proper syntax highlighting for Typst files in future uses! | |
363 | - | ||
364 | - | TYPST_TIP | |
365 | 300 | else | |
366 | 301 | # Normal mode - process all files | |
367 | 302 | fd '\.typ$' --type f "${fd_exclude_args[@]}" . > "$typst_files_list" | |
@@ -370,6 +305,174 @@ fi | |||
370 | 305 | ||
371 | 306 | total_files=$(wc -l < "$typst_files_list") | |
372 | 307 | ||
308 | + | # Function to extract Typst imports from a file | |
309 | + | extract_dependencies() { | |
310 | + | local file="$1" | |
311 | + | # Match both import formats: #import "file.typ" and #include "file.typ" | |
312 | + | # Also handle package imports: #import "@preview:0.1.0" | |
313 | + | # Return full paths to local imports, skipping package imports | |
314 | + | rg -U "#(import|include) \"(?!@)([^\"]+)\"" -r "$CWD/\$2" "$file" | \ | |
315 | + | while read -r dep; do | |
316 | + | # Resolve relative paths correctly | |
317 | + | if [[ "$dep" != /* ]]; then | |
318 | + | # If not absolute path, make it relative to the file's directory | |
319 | + | file_dir=$(dirname "$file") | |
320 | + | dep="$file_dir/$dep" | |
321 | + | fi | |
322 | + | ||
323 | + | # Normalize path | |
324 | + | dep=$(realpath --relative-to="$CWD" "$dep" 2>/dev/null || echo "$dep") | |
325 | + | ||
326 | + | # Only output if the dependency exists and is a .typ file | |
327 | + | if [[ "$dep" == *.typ ]] && [ -f "$dep" ]; then | |
328 | + | echo "$dep" | |
329 | + | fi | |
330 | + | done | sort -u # Sort and remove duplicates | |
331 | + | } | |
332 | + | ||
333 | + | # Function to build dependency graph for all files | |
334 | + | build_dependency_graph() { | |
335 | + | log_info "${ICON_CACHE} Building dependency graph..." | |
336 | + | ||
337 | + | # Create a deps file for each Typst file | |
338 | + | while read -r file; do | |
339 | + | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
340 | + | deps_file="${cache_dir}/deps/${file_hash}.deps" | |
341 | + | ||
342 | + | # Skip if deps file is fresh and not forced | |
343 | + | if [ "$force" -eq 0 ] && [ -f "$deps_file" ] && [ "$file" -ot "$deps_file" ]; then | |
344 | + | log_debug "Using cached dependencies for $file" | |
345 | + | continue | |
346 | + | fi | |
347 | + | ||
348 | + | # Extract dependencies and save to deps file | |
349 | + | extract_dependencies "$file" > "$deps_file" | |
350 | + | log_debug "Updated dependencies for $file" | |
351 | + | done < "$typst_files_list" | |
352 | + | ||
353 | + | # Create sorted compilation order based on dependencies | |
354 | + | # Files with fewer dependencies (or no dependencies) come first | |
355 | + | if [ -f "$temp_dir/compile_order.txt" ]; then | |
356 | + | rm "$temp_dir/compile_order.txt" | |
357 | + | fi | |
358 | + | ||
359 | + | # First pass: count dependencies for each file | |
360 | + | declare -A dep_counts | |
361 | + | while read -r file; do | |
362 | + | file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
363 | + | deps_file="${cache_dir}/deps/${file_hash}.deps" | |
364 | + | ||
365 | + | # Count dependencies | |
366 | + | if [ -f "$deps_file" ]; then | |
367 | + | dep_count=$(wc -l < "$deps_file") | |
368 | + | else | |
369 | + | dep_count=0 | |
370 | + | fi | |
371 | + | ||
372 | + | dep_counts["$file"]=$dep_count | |
373 | + | done < "$typst_files_list" | |
374 | + | ||
375 | + | # Second pass: sort files by dependency count (ascending) | |
376 | + | while read -r file; do | |
377 | + | echo "${dep_counts["$file"]} $file" | |
378 | + | done < "$typst_files_list" | sort -n | cut -d' ' -f2- > "$temp_dir/compile_order.txt" | |
379 | + | ||
380 | + | log_debug "Created optimized compilation order" | |
381 | + | } | |
382 | + | ||
383 | + | # Function to check if a file has changed since last compilation | |
384 | + | has_file_changed() { | |
385 | + | local file="$1" | |
386 | + | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
387 | + | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
388 | + | ||
389 | + | # Force recompilation if requested | |
390 | + | if [ "$force" -eq 1 ]; then | |
391 | + | log_debug "Force recompilation of $file" | |
392 | + | return 0 # File considered changed | |
393 | + | fi | |
394 | + | ||
395 | + | # Check if hash file exists | |
396 | + | if [ ! -f "$hash_file" ]; then | |
397 | + | log_debug "No previous hash for $file" | |
398 | + | return 0 # File considered changed | |
399 | + | fi | |
400 | + | ||
401 | + | # Compare current file hash with stored hash | |
402 | + | local current_hash=$(sha256sum "$file" | cut -d' ' -f1) | |
403 | + | local stored_hash=$(cat "$hash_file") | |
404 | + | ||
405 | + | if [ "$current_hash" != "$stored_hash" ]; then | |
406 | + | log_debug "File $file has changed since last compilation" | |
407 | + | return 0 # File has changed | |
408 | + | fi | |
409 | + | ||
410 | + | # Check if any dependencies have changed | |
411 | + | if [ "$dependency_scan" -eq 1 ]; then | |
412 | + | local deps_file="${cache_dir}/deps/${file_hash}.deps" | |
413 | + | if [ -f "$deps_file" ]; then | |
414 | + | while read -r dep; do | |
415 | + | if has_file_changed "$dep"; then | |
416 | + | log_debug "Dependency $dep of $file has changed" | |
417 | + | return 0 # Dependency has changed | |
418 | + | fi | |
419 | + | done < "$deps_file" | |
420 | + | fi | |
421 | + | fi | |
422 | + | ||
423 | + | log_debug "File $file and its dependencies are unchanged" | |
424 | + | return 1 # File and dependencies haven't changed | |
425 | + | } | |
426 | + | ||
427 | + | # Function to update file hash after compilation | |
428 | + | update_file_hash() { | |
429 | + | local file="$1" | |
430 | + | local file_hash=$(echo "$file" | md5sum | cut -d' ' -f1) | |
431 | + | local hash_file="${cache_dir}/hashes/${file_hash}.sha256" | |
432 | + | ||
433 | + | # Update hash file | |
434 | + | sha256sum "$file" | cut -d' ' -f1 > "$hash_file" | |
435 | + | log_debug "Updated hash for $file" | |
436 | + | } | |
437 | + | ||
438 | + | # Function to cache compiled PDF | |
439 | + | cache_pdf() { | |
440 | + | local src_file="$1" | |
441 | + | local pdf_file="$2" | |
442 | + | ||
443 | + | if [ ! -f "$pdf_file" ]; then | |
444 | + | log_debug "No PDF file to cache: $pdf_file" | |
445 | + | return 1 | |
446 | + | fi | |
447 | + | ||
448 | + | local file_hash=$(echo "$src_file" | md5sum | cut -d' ' -f1) | |
449 | + | local cached_pdf="${cache_dir}/pdfs/${file_hash}.pdf" | |
450 | + | ||
451 | + | # Copy PDF to cache | |
452 | + | cp "$pdf_file" "$cached_pdf" | |
453 | + | log_debug "Cached PDF for $src_file" | |
454 | + | return 0 | |
455 | + | } | |
456 | + | ||
457 | + | # Function to retrieve cached PDF | |
458 | + | get_cached_pdf() { | |
459 | + | local src_file="$1" | |
460 | + | local target_file="$2" | |
461 | + | ||
462 | + | local file_hash=$(echo "$src_file" | md5sum | cut -d' ' -f1) | |
463 | + | local cached_pdf="${cache_dir}/pdfs/${file_hash}.pdf" | |
464 | + | ||
465 | + | if [ ! -f "$cached_pdf" ]; then | |
466 | + | log_debug "No cached PDF for $src_file" | |
467 | + | return 1 | |
468 | + | fi | |
469 | + | ||
470 | + | # Copy cached PDF to target location | |
471 | + | cp "$cached_pdf" "$target_file" | |
472 | + | log_debug "Retrieved cached PDF for $src_file" | |
473 | + | return 0 | |
474 | + | } | |
475 | + | ||
373 | 476 | # Function to update progress bar during processing | |
374 | 477 | update_progress_during() { | |
375 | 478 | # If progress is disabled, do nothing | |
@@ -497,9 +600,11 @@ process_file() { | |||
497 | 600 | fi | |
498 | 601 | fi | |
499 | 602 | ||
603 | + | target_pdf="$target_dir/$basename.pdf" | |
604 | + | ||
500 | 605 | # Skip if PDF is newer than source and --skip-newer is specified | |
501 | - | if [ "$skip_newer" -eq 1 ] && [ -f "$target_dir/$basename.pdf" ]; then | |
502 | - | if [ "$typfile" -ot "$target_dir/$basename.pdf" ] && [ "$force" -eq 0 ]; then | |
606 | + | if [ "$skip_newer" -eq 1 ] && [ -f "$target_pdf" ]; then | |
607 | + | if [ "$typfile" -ot "$target_pdf" ] && [ "$force" -eq 0 ]; then | |
503 | 608 | log_debug "Skipping $typfile (PDF is newer)" | |
504 | 609 | touch "$progress_dir/$file_id" | |
505 | 610 | update_progress_during | |
@@ -507,6 +612,19 @@ process_file() { | |||
507 | 612 | fi | |
508 | 613 | fi | |
509 | 614 | ||
615 | + | # Check if file has changed (if caching is enabled) | |
616 | + | if [ "$use_cache" -eq 1 ] && [ "$dry_run" -eq 0 ]; then | |
617 | + | if ! has_file_changed "$typfile"; then | |
618 | + | # Try to retrieve cached PDF | |
619 | + | if get_cached_pdf "$typfile" "$target_pdf"; then | |
620 | + | log_debug "Using cached PDF for $typfile" | |
621 | + | touch "$progress_dir/$file_id" | |
622 | + | update_progress_during | |
623 | + | return 0 | |
624 | + | fi | |
625 | + | fi | |
626 | + | fi | |
627 | + | ||
510 | 628 | # Create a temporary file for capturing compiler output | |
511 | 629 | temp_output="$temp_dir/output_${file_id}.log" | |
512 | 630 | ||
@@ -519,7 +637,7 @@ process_file() { | |||
519 | 637 | # In dry run mode, just log what would be done | |
520 | 638 | if [ "$dry_run" -eq 1 ]; then | |
521 | 639 | log_debug "[DRY RUN] Would compile: $typfile" | |
522 | - | log_debug "[DRY RUN] Would move to: $target_dir/$basename.pdf" | |
640 | + | log_debug "[DRY RUN] Would move to: $target_pdf" | |
523 | 641 | touch "$progress_dir/$file_id" | |
524 | 642 | update_progress_during | |
525 | 643 | return 0 | |
@@ -545,7 +663,15 @@ process_file() { | |||
545 | 663 | ) 201>"$compile_log.lock" | |
546 | 664 | else | |
547 | 665 | # Check if the output PDF exists | |
548 | - | if [ -f "$typdir/$basename.pdf" ]; then | |
666 | + | temp_pdf="$typdir/$basename.pdf" | |
667 | + | if [ -f "$temp_pdf" ]; then | |
668 | + | # Cache the PDF if caching is enabled | |
669 | + | if [ "$use_cache" -eq 1 ]; then | |
670 | + | cache_pdf "$typfile" "$temp_pdf" | |
671 | + | # Update file hash | |
672 | + | update_file_hash "$typfile" | |
673 | + | fi | |
674 | + | ||
549 | 675 | # Try to move the output PDF to the output directory | |
550 | 676 | move_header="$temp_dir/move_${file_id}.header" | |
551 | 677 | { | |
@@ -553,15 +679,15 @@ process_file() { | |||
553 | 679 | echo "$(date)" | |
554 | 680 | } > "$move_header" | |
555 | 681 | ||
556 | - | if ! mv "$typdir/$basename.pdf" "$target_dir/" 2> "$temp_output.move_err"; then | |
557 | - | echo "$typfile -> $target_dir/$basename.pdf" > "$move_failures_dir/$file_id" | |
682 | + | if ! mv "$temp_pdf" "$target_dir/" 2> "$temp_output.move_err"; then | |
683 | + | echo "$typfile -> $target_pdf" > "$move_failures_dir/$file_id" | |
558 | 684 | log_debug "Failed to move $basename.pdf to $target_dir/" | |
559 | 685 | ||
560 | 686 | # Lock the log file to avoid concurrent writes corrupting it | |
561 | 687 | ( | |
562 | 688 | flock -w 1 202 | |
563 | 689 | cat "$move_header" "$temp_output.move_err" >> "$move_log" | |
564 | - | echo "Failed to move $typdir/$basename.pdf to $target_dir/" >> "$move_log" | |
690 | + | echo "Failed to move $temp_pdf to $target_dir/" >> "$move_log" | |
565 | 691 | ) 202>"$move_log.lock" | |
566 | 692 | else | |
567 | 693 | log_debug "Moved $basename.pdf to $target_dir/" | |
@@ -594,6 +720,10 @@ export -f log_info | |||
594 | 720 | export -f log_warning | |
595 | 721 | export -f log_error | |
596 | 722 | export -f log_success | |
723 | + | export -f has_file_changed | |
724 | + | export -f update_file_hash | |
725 | + | export -f cache_pdf | |
726 | + | export -f get_cached_pdf | |
597 | 727 | export CWD | |
598 | 728 | export temp_dir | |
599 | 729 | export compile_failures_dir | |
@@ -603,9 +733,10 @@ export final_progress_file | |||
603 | 733 | export progress_lock | |
604 | 734 | export compile_log | |
605 | 735 | export move_log | |
736 | + | export cache_dir | |
606 | 737 | export total_files | |
607 | 738 | export GREEN BLUE YELLOW RED CYAN PURPLE NC BOLD | |
608 | - | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG | |
739 | + | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG ICON_CACHE | |
609 | 740 | export verbose | |
610 | 741 | export quiet | |
611 | 742 | export output_dir | |
@@ -614,6 +745,8 @@ export dry_run | |||
614 | 745 | export skip_newer | |
615 | 746 | export show_progress | |
616 | 747 | export force | |
748 | + | export use_cache | |
749 | + | export dependency_scan | |
617 | 750 | ||
618 | 751 | # Determine the number of CPU cores and use that many parallel jobs (if not specified) | |
619 | 752 | if [ "$jobs" -eq 0 ]; then | |
@@ -621,6 +754,14 @@ if [ "$jobs" -eq 0 ]; then | |||
621 | 754 | fi | |
622 | 755 | log_info "Using ${BOLD}$jobs${NC} parallel jobs for compilation" | |
623 | 756 | ||
757 | + | # Build dependency graph if dependency scanning is enabled | |
758 | + | if [ "$dependency_scan" -eq 1 ] && [ "$total_files" -gt 0 ]; then | |
759 | + | build_dependency_graph | |
760 | + | # Use optimized compilation order | |
761 | + | cp "$temp_dir/compile_order.txt" "$typst_files_list" | |
762 | + | log_info "Optimized compilation order based on dependencies" | |
763 | + | fi | |
764 | + | ||
624 | 765 | # Initialize progress bar if showing progress | |
625 | 766 | if [ "$show_progress" -eq 1 ]; then | |
626 | 767 | update_progress_during | |
@@ -666,6 +807,16 @@ if [ "$total_files" -gt 0 ]; then | |||
666 | 807 | echo -e "${BLUE}See $move_log for detailed error messages.${NC}" | |
667 | 808 | fi | |
668 | 809 | fi | |
810 | + | ||
811 | + | # Cache usage statistics (if enabled and not in quiet mode) | |
812 | + | if [ "$use_cache" -eq 1 ] && [ "$quiet" -eq 0 ]; then | |
813 | + | cache_files=$(find "${cache_dir}/pdfs" -type f | wc -l) | |
814 | + | cache_size=$(du -sh "${cache_dir}" 2>/dev/null | cut -f1) | |
815 | + | echo -e "\n${CYAN}${ICON_CACHE} Cache statistics:${NC}" | |
816 | + | echo -e " - Cached files: ${BOLD}$cache_files${NC}" | |
817 | + | echo -e " - Cache size: ${BOLD}$cache_size${NC}" | |
818 | + | echo -e " - Cache location: ${BOLD}$cache_dir${NC}" | |
819 | + | fi | |
669 | 820 | else | |
670 | 821 | log_warning "No .typ files found to process." | |
671 | 822 | fi |
Waffle1412 revised this gist . Go to revision
1 file changed, 677 insertions
compile_typst.sh(file created)
@@ -0,0 +1,677 @@ | |||
1 | + | #!/usr/bin/env bash | |
2 | + | ||
3 | + | # Default values | |
4 | + | jobs=0 # 0 means auto-detect | |
5 | + | output_dir="pdfs" | |
6 | + | verbose=0 | |
7 | + | quiet=0 | |
8 | + | create_dirs=0 | |
9 | + | dry_run=0 | |
10 | + | skip_newer=0 | |
11 | + | show_progress=1 | |
12 | + | force=0 | |
13 | + | select_mode=0 | |
14 | + | compile_log="typst_compile_errors.log" | |
15 | + | move_log="typst_move_errors.log" | |
16 | + | declare -a exclude_patterns | |
17 | + | ||
18 | + | # Show usage/help information | |
19 | + | show_help() { | |
20 | + | cat << EOF | |
21 | + | Usage: $(basename "$0") [OPTIONS] | |
22 | + | ||
23 | + | Compile Typst files and move generated PDFs to a designated directory. | |
24 | + | ||
25 | + | Options: | |
26 | + | -j, --jobs NUM Number of parallel jobs (default: auto-detect) | |
27 | + | -o, --output-dir DIR Output directory name (default: pdfs) | |
28 | + | -v, --verbose Increase verbosity | |
29 | + | -q, --quiet Suppress most output | |
30 | + | -c, --create-dirs Create output directories if they don't exist | |
31 | + | -d, --dry-run Show what would be done without doing it | |
32 | + | -s, --skip-newer Skip compilation if PDF is newer than source | |
33 | + | -S, --select Interactive selection mode using skim | |
34 | + | -f, --force Force compilation even if PDF exists | |
35 | + | --no-progress Disable progress bar | |
36 | + | --compile-log FILE Custom location for compilation log (default: $compile_log) | |
37 | + | --move-log FILE Custom location for move log (default: $move_log) | |
38 | + | -e, --exclude PATTERN Exclude files matching pattern (can be used multiple times) | |
39 | + | -h, --help Show this help message and exit | |
40 | + | ||
41 | + | Examples: | |
42 | + | $(basename "$0") -j 4 -o output -c | |
43 | + | $(basename "$0") --verbose --skip-newer | |
44 | + | $(basename "$0") -S # Select files to compile interactively | |
45 | + | $(basename "$0") -e "**/test/**" -e "**/draft/**" | |
46 | + | EOF | |
47 | + | } | |
48 | + | ||
49 | + | # Parse command line arguments | |
50 | + | while [[ $# -gt 0 ]]; do | |
51 | + | case $1 in | |
52 | + | -j|--jobs) | |
53 | + | jobs="$2" | |
54 | + | shift 2 | |
55 | + | ;; | |
56 | + | -o|--output-dir) | |
57 | + | output_dir="$2" | |
58 | + | shift 2 | |
59 | + | ;; | |
60 | + | -v|--verbose) | |
61 | + | verbose=1 | |
62 | + | shift | |
63 | + | ;; | |
64 | + | -q|--quiet) | |
65 | + | quiet=1 | |
66 | + | shift | |
67 | + | ;; | |
68 | + | -c|--create-dirs) | |
69 | + | create_dirs=1 | |
70 | + | shift | |
71 | + | ;; | |
72 | + | -d|--dry-run) | |
73 | + | dry_run=1 | |
74 | + | shift | |
75 | + | ;; | |
76 | + | -s|--skip-newer) | |
77 | + | skip_newer=1 | |
78 | + | shift | |
79 | + | ;; | |
80 | + | -S|--select) | |
81 | + | select_mode=1 | |
82 | + | shift | |
83 | + | ;; | |
84 | + | -f|--force) | |
85 | + | force=1 | |
86 | + | shift | |
87 | + | ;; | |
88 | + | --no-progress) | |
89 | + | show_progress=0 | |
90 | + | shift | |
91 | + | ;; | |
92 | + | --compile-log) | |
93 | + | compile_log="$2" | |
94 | + | shift 2 | |
95 | + | ;; | |
96 | + | --move-log) | |
97 | + | move_log="$2" | |
98 | + | shift 2 | |
99 | + | ;; | |
100 | + | -e|--exclude) | |
101 | + | exclude_patterns+=("$2") | |
102 | + | shift 2 | |
103 | + | ;; | |
104 | + | -h|--help) | |
105 | + | show_help | |
106 | + | exit 0 | |
107 | + | ;; | |
108 | + | *) | |
109 | + | echo "Unknown option: $1" | |
110 | + | show_help | |
111 | + | exit 1 | |
112 | + | ;; | |
113 | + | esac | |
114 | + | done | |
115 | + | ||
116 | + | # Check for conflicting options | |
117 | + | if [ "$verbose" -eq 1 ] && [ "$quiet" -eq 1 ]; then | |
118 | + | echo "Error: Cannot use both --verbose and --quiet" | |
119 | + | exit 1 | |
120 | + | fi | |
121 | + | ||
122 | + | # Check if required tools are installed | |
123 | + | check_tool() { | |
124 | + | if ! command -v "$1" &> /dev/null; then | |
125 | + | echo "$1 is not installed. Please install it first." | |
126 | + | echo "On most systems: $2" | |
127 | + | exit 1 | |
128 | + | fi | |
129 | + | } | |
130 | + | ||
131 | + | check_tool "fd" "cargo install fd-find or apt/brew install fd-find" | |
132 | + | check_tool "rg" "cargo install ripgrep or apt/brew install ripgrep" | |
133 | + | check_tool "parallel" "apt/brew install parallel" | |
134 | + | ||
135 | + | # Check for skim and bat if select mode is enabled | |
136 | + | if [ "$select_mode" -eq 1 ]; then | |
137 | + | check_tool "sk" "cargo install skim or apt/brew install skim" | |
138 | + | check_tool "bat" "cargo install bat or apt/brew install bat" | |
139 | + | fi | |
140 | + | ||
141 | + | # ANSI color codes | |
142 | + | GREEN='\033[0;32m' | |
143 | + | BLUE='\033[0;34m' | |
144 | + | YELLOW='\033[0;33m' | |
145 | + | RED='\033[0;31m' | |
146 | + | CYAN='\033[0;36m' | |
147 | + | PURPLE='\033[0;35m' | |
148 | + | NC='\033[0m' # No Color | |
149 | + | BOLD='\033[1m' | |
150 | + | ||
151 | + | # Nerd font icons | |
152 | + | ICON_SUCCESS=" " | |
153 | + | ICON_ERROR=" " | |
154 | + | ICON_WORKING=" " | |
155 | + | ICON_COMPILE=" " | |
156 | + | ICON_MOVE=" " | |
157 | + | ICON_COMPLETE=" " | |
158 | + | ICON_SUMMARY=" " | |
159 | + | ICON_INFO=" " | |
160 | + | ICON_DEBUG=" " | |
161 | + | ICON_SELECT=" " | |
162 | + | ||
163 | + | # Logging functions | |
164 | + | log_debug() { | |
165 | + | if [ "$verbose" -eq 1 ]; then | |
166 | + | echo -e "${BLUE}$ICON_DEBUG${NC} $*" | |
167 | + | fi | |
168 | + | } | |
169 | + | ||
170 | + | log_info() { | |
171 | + | if [ "$quiet" -eq 0 ]; then | |
172 | + | echo -e "${CYAN}$ICON_INFO${NC} $*" | |
173 | + | fi | |
174 | + | } | |
175 | + | ||
176 | + | log_warning() { | |
177 | + | if [ "$quiet" -eq 0 ]; then | |
178 | + | echo -e "${YELLOW}⚠️ ${NC} $*" | |
179 | + | fi | |
180 | + | } | |
181 | + | ||
182 | + | log_error() { | |
183 | + | echo -e "${RED}${BOLD}$ICON_ERROR${NC} $*" | |
184 | + | } | |
185 | + | ||
186 | + | log_success() { | |
187 | + | if [ "$quiet" -eq 0 ]; then | |
188 | + | echo -e "${GREEN}${BOLD}$ICON_SUCCESS${NC} $*" | |
189 | + | fi | |
190 | + | } | |
191 | + | ||
192 | + | # Create a directory for temporary files | |
193 | + | temp_dir=$(mktemp -d) | |
194 | + | compile_failures_dir="$temp_dir/compile_failures" | |
195 | + | move_failures_dir="$temp_dir/move_failures" | |
196 | + | progress_dir="$temp_dir/progress" | |
197 | + | mkdir -p "$compile_failures_dir" "$move_failures_dir" "$progress_dir" | |
198 | + | ||
199 | + | # Create a lock file for progress updates and a flag for final progress | |
200 | + | progress_lock="/tmp/typst_progress_lock" | |
201 | + | final_progress_file="$temp_dir/final_progress" | |
202 | + | touch "$final_progress_file" | |
203 | + | ||
204 | + | # Initialize log files | |
205 | + | > "$compile_log" | |
206 | + | > "$move_log" | |
207 | + | ||
208 | + | # Store current working directory | |
209 | + | CWD=$(pwd) | |
210 | + | ||
211 | + | log_info "Starting Typst compilation process..." | |
212 | + | ||
213 | + | # Build exclude arguments for fd | |
214 | + | fd_exclude_args=() | |
215 | + | for pattern in "${exclude_patterns[@]}"; do | |
216 | + | fd_exclude_args+=("-E" "$pattern") | |
217 | + | done | |
218 | + | ||
219 | + | # Create a list of files to process | |
220 | + | typst_files_list="$temp_dir/typst_files.txt" | |
221 | + | ||
222 | + | # Create a custom bat configuration for Typst syntax | |
223 | + | setup_bat_for_typst() { | |
224 | + | bat_config_dir="$temp_dir/bat_config" | |
225 | + | mkdir -p "$bat_config_dir/syntaxes" | |
226 | + | ||
227 | + | # Create a basic syntax mapping file for Typst | |
228 | + | cat > "$bat_config_dir/syntaxes/typst.sublime-syntax" << 'TYPST_SYNTAX' | |
229 | + | %YAML 1.2 | |
230 | + | --- | |
231 | + | name: Typst | |
232 | + | file_extensions: | |
233 | + | - typ | |
234 | + | scope: source.typst | |
235 | + | ||
236 | + | contexts: | |
237 | + | main: | |
238 | + | # Comments | |
239 | + | - match: /\* | |
240 | + | scope: comment.block.typst | |
241 | + | push: block_comment | |
242 | + | ||
243 | + | - match: // | |
244 | + | scope: comment.line.double-slash.typst | |
245 | + | push: line_comment | |
246 | + | ||
247 | + | # Strings | |
248 | + | - match: '"' | |
249 | + | scope: punctuation.definition.string.begin.typst | |
250 | + | push: double_string | |
251 | + | ||
252 | + | # Math | |
253 | + | - match: '\$' | |
254 | + | scope: punctuation.definition.math.begin.typst | |
255 | + | push: math | |
256 | + | ||
257 | + | # Functions | |
258 | + | - match: '#([a-zA-Z][a-zA-Z0-9_]*)' | |
259 | + | scope: entity.name.function.typst | |
260 | + | ||
261 | + | # Variables | |
262 | + | - match: '\b([a-zA-Z][a-zA-Z0-9_]*)\s*:' | |
263 | + | scope: variable.other.typst | |
264 | + | ||
265 | + | # Keywords | |
266 | + | - match: '\b(let|set|show|if|else|for|in|while|return|import|include|at|do|not|and|or|none|auto)\b' | |
267 | + | scope: keyword.control.typst | |
268 | + | ||
269 | + | block_comment: | |
270 | + | - match: \*/ | |
271 | + | scope: comment.block.typst | |
272 | + | pop: true | |
273 | + | - match: . | |
274 | + | scope: comment.block.typst | |
275 | + | ||
276 | + | line_comment: | |
277 | + | - match: $ | |
278 | + | pop: true | |
279 | + | - match: . | |
280 | + | scope: comment.line.double-slash.typst | |
281 | + | ||
282 | + | double_string: | |
283 | + | - match: '"' | |
284 | + | scope: punctuation.definition.string.end.typst | |
285 | + | pop: true | |
286 | + | - match: \\. | |
287 | + | scope: constant.character.escape.typst | |
288 | + | - match: . | |
289 | + | scope: string.quoted.double.typst | |
290 | + | ||
291 | + | math: | |
292 | + | - match: '\$' | |
293 | + | scope: punctuation.definition.math.end.typst | |
294 | + | pop: true | |
295 | + | - match: . | |
296 | + | scope: markup.math.typst | |
297 | + | TYPST_SYNTAX | |
298 | + | ||
299 | + | echo "$bat_config_dir" | |
300 | + | } | |
301 | + | ||
302 | + | if [ "$select_mode" -eq 1 ]; then | |
303 | + | log_info "${PURPLE}$ICON_SELECT${NC} Interactive selection mode enabled" | |
304 | + | log_info "Use TAB to select multiple files, ENTER to confirm" | |
305 | + | ||
306 | + | # Set up bat configuration for Typst syntax highlighting | |
307 | + | bat_config=$(setup_bat_for_typst) | |
308 | + | export BAT_CONFIG_PATH="$bat_config" | |
309 | + | ||
310 | + | # Use skim with bat for preview to select files | |
311 | + | selected_files="$temp_dir/selected_files.txt" | |
312 | + | ||
313 | + | # Find all eligible .typ files for selection | |
314 | + | fd '\.typ$' --type f "${fd_exclude_args[@]}" . > "$typst_files_list.all" | |
315 | + | ||
316 | + | # Check if we found any files | |
317 | + | if [ ! -s "$typst_files_list.all" ]; then | |
318 | + | log_error "No .typ files found matching your criteria." | |
319 | + | rm -rf "$temp_dir" | |
320 | + | exit 0 | |
321 | + | fi | |
322 | + | ||
323 | + | # Prepare preview command for skim: use bat with custom syntax for .typ files | |
324 | + | preview_cmd="bat --color=always --style=numbers --map-syntax='*.typ:Markdown' {} 2>/dev/null || cat {}" | |
325 | + | ||
326 | + | # Use skim with bat preview to select files | |
327 | + | cat "$typst_files_list.all" | sk --multi \ | |
328 | + | --preview "$preview_cmd" \ | |
329 | + | --preview-window "right:70%" \ | |
330 | + | --height "80%" \ | |
331 | + | --prompt "Select .typ files to compile: " \ | |
332 | + | --header "TAB: Select multiple files, ENTER: Confirm, CTRL-C: Cancel" \ | |
333 | + | --no-mouse > "$selected_files" | |
334 | + | ||
335 | + | # Check if user selected any files | |
336 | + | if [ ! -s "$selected_files" ]; then | |
337 | + | log_error "No files selected. Exiting." | |
338 | + | rm -rf "$temp_dir" | |
339 | + | exit 0 | |
340 | + | fi | |
341 | + | ||
342 | + | # Use the selected files instead of all discovered files | |
343 | + | cp "$selected_files" "$typst_files_list" | |
344 | + | total_selected=$(wc -l < "$typst_files_list") | |
345 | + | total_available=$(wc -l < "$typst_files_list.all") | |
346 | + | ||
347 | + | log_info "Selected ${BOLD}$total_selected${NC} out of ${BOLD}$total_available${NC} available files" | |
348 | + | ||
349 | + | # Display tip about better Typst syntax highlighting | |
350 | + | cat << 'TYPST_TIP' | |
351 | + | ||
352 | + | 📝 To get proper Typst syntax highlighting in bat: | |
353 | + | ||
354 | + | 1. Create a custom Typst syntax file: | |
355 | + | mkdir -p ~/.config/bat/syntaxes | |
356 | + | curl -L https://raw.githubusercontent.com/typst/typst-vs-code/main/syntaxes/typst.tmLanguage.json \ | |
357 | + | -o ~/.config/bat/syntaxes/typst.tmLanguage.json | |
358 | + | ||
359 | + | 2. Build bat's syntax cache: | |
360 | + | bat cache --build | |
361 | + | ||
362 | + | This will provide proper syntax highlighting for Typst files in future uses! | |
363 | + | ||
364 | + | TYPST_TIP | |
365 | + | else | |
366 | + | # Normal mode - process all files | |
367 | + | fd '\.typ$' --type f "${fd_exclude_args[@]}" . > "$typst_files_list" | |
368 | + | log_info "Found ${BOLD}$(wc -l < "$typst_files_list")${NC} Typst files to process" | |
369 | + | fi | |
370 | + | ||
371 | + | total_files=$(wc -l < "$typst_files_list") | |
372 | + | ||
373 | + | # Function to update progress bar during processing | |
374 | + | update_progress_during() { | |
375 | + | # If progress is disabled, do nothing | |
376 | + | if [ "$show_progress" -eq 0 ]; then | |
377 | + | return 0 | |
378 | + | fi | |
379 | + | ||
380 | + | ( | |
381 | + | # Try to acquire lock, but don't wait if busy | |
382 | + | flock -n 200 || return 0 | |
383 | + | ||
384 | + | completed=$(find "$progress_dir" -type f | wc -l) | |
385 | + | percent=$((completed * 100 / total_files)) | |
386 | + | bar_length=50 | |
387 | + | filled_length=$((bar_length * completed / total_files)) | |
388 | + | ||
389 | + | # Create the progress bar | |
390 | + | bar="" | |
391 | + | for ((i=0; i<bar_length; i++)); do | |
392 | + | if [ $i -lt $filled_length ]; then | |
393 | + | bar="${bar}█" | |
394 | + | else | |
395 | + | bar="${bar}░" | |
396 | + | fi | |
397 | + | done | |
398 | + | ||
399 | + | # Calculate success and failure counts | |
400 | + | success=$((completed - $(find "$compile_failures_dir" "$move_failures_dir" -type f | wc -l))) | |
401 | + | compile_fails=$(find "$compile_failures_dir" -type f | wc -l) | |
402 | + | move_fails=$(find "$move_failures_dir" -type f | wc -l) | |
403 | + | ||
404 | + | # Clear the previous line and print the updated progress | |
405 | + | echo -ne "\r\033[K" | |
406 | + | echo -ne "${PURPLE}$ICON_WORKING Progress: ${GREEN}$bar ${BOLD}${percent}%${NC} " | |
407 | + | echo -ne "[${GREEN}${success}✓${NC}|${RED}${compile_fails}✗${NC}|${YELLOW}${move_fails}!${NC}] " | |
408 | + | echo -ne "${BLUE}($completed/$total_files)${NC}" | |
409 | + | ||
410 | + | # If all files are processed, print the completion message ONLY ONCE | |
411 | + | if [ $completed -eq $total_files ] && [ ! -e "$final_progress_file.done" ]; then | |
412 | + | touch "$final_progress_file.done" | |
413 | + | echo -e "\n${GREEN}${BOLD}$ICON_COMPLETE All files processed!${NC}" | |
414 | + | fi | |
415 | + | ) 200>"$progress_lock" | |
416 | + | } | |
417 | + | ||
418 | + | # Function to show final progress (called only once at the end) | |
419 | + | show_final_progress() { | |
420 | + | # If progress is disabled, do nothing | |
421 | + | if [ "$show_progress" -eq 0 ]; then | |
422 | + | return 0 | |
423 | + | fi | |
424 | + | ||
425 | + | ( | |
426 | + | flock -w 1 200 | |
427 | + | ||
428 | + | # Only proceed if the final progress hasn't been shown yet | |
429 | + | if [ -e "$final_progress_file.done" ]; then | |
430 | + | return 0 | |
431 | + | fi | |
432 | + | ||
433 | + | completed=$(find "$progress_dir" -type f | wc -l) | |
434 | + | percent=$((completed * 100 / total_files)) | |
435 | + | bar_length=50 | |
436 | + | filled_length=$((bar_length * completed / total_files)) | |
437 | + | ||
438 | + | # Create the progress bar | |
439 | + | bar="" | |
440 | + | for ((i=0; i<bar_length; i++)); do | |
441 | + | if [ $i -lt $filled_length ]; then | |
442 | + | bar="${bar}█" | |
443 | + | else | |
444 | + | bar="${bar}░" | |
445 | + | fi | |
446 | + | done | |
447 | + | ||
448 | + | # Calculate success and failure counts | |
449 | + | success=$((completed - $(find "$compile_failures_dir" "$move_failures_dir" -type f | wc -l))) | |
450 | + | compile_fails=$(find "$compile_failures_dir" -type f | wc -l) | |
451 | + | move_fails=$(find "$move_failures_dir" -type f | wc -l) | |
452 | + | ||
453 | + | # Clear the previous line and print the updated progress | |
454 | + | echo -ne "\r\033[K" | |
455 | + | echo -ne "${PURPLE}$ICON_WORKING Progress: ${GREEN}$bar ${BOLD}${percent}%${NC} " | |
456 | + | echo -ne "[${GREEN}${success}✓${NC}|${RED}${compile_fails}✗${NC}|${YELLOW}${move_fails}!${NC}] " | |
457 | + | echo -ne "${BLUE}($completed/$total_files)${NC}" | |
458 | + | ||
459 | + | # Mark final progress as shown | |
460 | + | touch "$final_progress_file.done" | |
461 | + | ||
462 | + | # If all files are processed, print the completion message | |
463 | + | if [ $completed -eq $total_files ]; then | |
464 | + | echo -e "\n${GREEN}${BOLD}$ICON_COMPLETE All files processed!${NC}" | |
465 | + | fi | |
466 | + | ) 200>"$progress_lock" | |
467 | + | } | |
468 | + | ||
469 | + | # Function to process a single .typ file | |
470 | + | process_file() { | |
471 | + | typfile="$1" | |
472 | + | file_id=$(echo "$typfile" | md5sum | cut -d' ' -f1) | |
473 | + | ||
474 | + | # Get the directory containing the .typ file | |
475 | + | typdir=$(dirname "$typfile") | |
476 | + | # Get the filename without path | |
477 | + | filename=$(basename "$typfile") | |
478 | + | # Get the filename without extension | |
479 | + | basename="${filename%.typ}" | |
480 | + | ||
481 | + | # Check if output directory exists or should be created | |
482 | + | target_dir="$typdir/$output_dir" | |
483 | + | if [ ! -d "$target_dir" ]; then | |
484 | + | if [ "$create_dirs" -eq 1 ]; then | |
485 | + | if [ "$dry_run" -eq 0 ]; then | |
486 | + | mkdir -p "$target_dir" | |
487 | + | log_debug "Created directory: $target_dir" | |
488 | + | else | |
489 | + | log_debug "[DRY RUN] Would create directory: $target_dir" | |
490 | + | fi | |
491 | + | else | |
492 | + | # Skip this file if output directory doesn't exist and --create-dirs not specified | |
493 | + | log_debug "Skipping $typfile (no $output_dir directory)" | |
494 | + | touch "$progress_dir/$file_id" | |
495 | + | update_progress_during | |
496 | + | return 0 | |
497 | + | fi | |
498 | + | fi | |
499 | + | ||
500 | + | # Skip if PDF is newer than source and --skip-newer is specified | |
501 | + | if [ "$skip_newer" -eq 1 ] && [ -f "$target_dir/$basename.pdf" ]; then | |
502 | + | if [ "$typfile" -ot "$target_dir/$basename.pdf" ] && [ "$force" -eq 0 ]; then | |
503 | + | log_debug "Skipping $typfile (PDF is newer)" | |
504 | + | touch "$progress_dir/$file_id" | |
505 | + | update_progress_during | |
506 | + | return 0 | |
507 | + | fi | |
508 | + | fi | |
509 | + | ||
510 | + | # Create a temporary file for capturing compiler output | |
511 | + | temp_output="$temp_dir/output_${file_id}.log" | |
512 | + | ||
513 | + | # Add a header to the log before compilation | |
514 | + | { | |
515 | + | echo -e "\n===== COMPILING: $typfile =====" | |
516 | + | echo "$(date)" | |
517 | + | } > "$temp_output.header" | |
518 | + | ||
519 | + | # In dry run mode, just log what would be done | |
520 | + | if [ "$dry_run" -eq 1 ]; then | |
521 | + | log_debug "[DRY RUN] Would compile: $typfile" | |
522 | + | log_debug "[DRY RUN] Would move to: $target_dir/$basename.pdf" | |
523 | + | touch "$progress_dir/$file_id" | |
524 | + | update_progress_during | |
525 | + | return 0 | |
526 | + | fi | |
527 | + | ||
528 | + | # Compile the .typ file using typst with --root flag and capture all output | |
529 | + | if ! typst compile --root "$CWD" "$typfile" > "$temp_output.stdout" 2> "$temp_output.stderr"; then | |
530 | + | # Store the failure | |
531 | + | echo "$typfile" > "$compile_failures_dir/$file_id" | |
532 | + | log_debug "Compilation failed for $typfile" | |
533 | + | ||
534 | + | # Combine stdout and stderr | |
535 | + | cat "$temp_output.stdout" "$temp_output.stderr" > "$temp_output.combined" | |
536 | + | ||
537 | + | # Filter the output to only include error messages using ripgrep | |
538 | + | rg "error:" -A 20 "$temp_output.combined" > "$temp_output.errors" || true | |
539 | + | ||
540 | + | # Lock the log file to avoid concurrent writes corrupting it | |
541 | + | ( | |
542 | + | flock -w 1 201 | |
543 | + | cat "$temp_output.header" "$temp_output.errors" >> "$compile_log" | |
544 | + | echo -e "\n" >> "$compile_log" | |
545 | + | ) 201>"$compile_log.lock" | |
546 | + | else | |
547 | + | # Check if the output PDF exists | |
548 | + | if [ -f "$typdir/$basename.pdf" ]; then | |
549 | + | # Try to move the output PDF to the output directory | |
550 | + | move_header="$temp_dir/move_${file_id}.header" | |
551 | + | { | |
552 | + | echo -e "\n===== MOVING: $typfile =====" | |
553 | + | echo "$(date)" | |
554 | + | } > "$move_header" | |
555 | + | ||
556 | + | if ! mv "$typdir/$basename.pdf" "$target_dir/" 2> "$temp_output.move_err"; then | |
557 | + | echo "$typfile -> $target_dir/$basename.pdf" > "$move_failures_dir/$file_id" | |
558 | + | log_debug "Failed to move $basename.pdf to $target_dir/" | |
559 | + | ||
560 | + | # Lock the log file to avoid concurrent writes corrupting it | |
561 | + | ( | |
562 | + | flock -w 1 202 | |
563 | + | cat "$move_header" "$temp_output.move_err" >> "$move_log" | |
564 | + | echo "Failed to move $typdir/$basename.pdf to $target_dir/" >> "$move_log" | |
565 | + | ) 202>"$move_log.lock" | |
566 | + | else | |
567 | + | log_debug "Moved $basename.pdf to $target_dir/" | |
568 | + | fi | |
569 | + | else | |
570 | + | # This is a fallback check in case typst doesn't return error code | |
571 | + | echo "$typfile" > "$compile_failures_dir/$file_id" | |
572 | + | log_debug "Compilation completed without errors but no PDF was generated for $typfile" | |
573 | + | ||
574 | + | # Lock the log file to avoid concurrent writes corrupting it | |
575 | + | ( | |
576 | + | flock -w 1 201 | |
577 | + | echo "Compilation completed without errors but no PDF was generated" >> "$compile_log" | |
578 | + | ) 201>"$compile_log.lock" | |
579 | + | fi | |
580 | + | fi | |
581 | + | ||
582 | + | # Mark this file as processed (for progress tracking) | |
583 | + | touch "$progress_dir/$file_id" | |
584 | + | ||
585 | + | # Update the progress bar | |
586 | + | update_progress_during | |
587 | + | } | |
588 | + | ||
589 | + | export -f process_file | |
590 | + | export -f update_progress_during | |
591 | + | export -f show_final_progress | |
592 | + | export -f log_debug | |
593 | + | export -f log_info | |
594 | + | export -f log_warning | |
595 | + | export -f log_error | |
596 | + | export -f log_success | |
597 | + | export CWD | |
598 | + | export temp_dir | |
599 | + | export compile_failures_dir | |
600 | + | export move_failures_dir | |
601 | + | export progress_dir | |
602 | + | export final_progress_file | |
603 | + | export progress_lock | |
604 | + | export compile_log | |
605 | + | export move_log | |
606 | + | export total_files | |
607 | + | export GREEN BLUE YELLOW RED CYAN PURPLE NC BOLD | |
608 | + | export ICON_SUCCESS ICON_ERROR ICON_WORKING ICON_COMPILE ICON_MOVE ICON_COMPLETE ICON_SUMMARY ICON_INFO ICON_DEBUG | |
609 | + | export verbose | |
610 | + | export quiet | |
611 | + | export output_dir | |
612 | + | export create_dirs | |
613 | + | export dry_run | |
614 | + | export skip_newer | |
615 | + | export show_progress | |
616 | + | export force | |
617 | + | ||
618 | + | # Determine the number of CPU cores and use that many parallel jobs (if not specified) | |
619 | + | if [ "$jobs" -eq 0 ]; then | |
620 | + | jobs=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) | |
621 | + | fi | |
622 | + | log_info "Using ${BOLD}$jobs${NC} parallel jobs for compilation" | |
623 | + | ||
624 | + | # Initialize progress bar if showing progress | |
625 | + | if [ "$show_progress" -eq 1 ]; then | |
626 | + | update_progress_during | |
627 | + | fi | |
628 | + | ||
629 | + | # Process files in parallel with --will-cite to suppress citation notice | |
630 | + | if [ "$total_files" -gt 0 ]; then | |
631 | + | cat "$typst_files_list" | parallel --will-cite --jobs "$jobs" process_file | |
632 | + | ||
633 | + | # Wait a moment for any remaining progress updates to complete | |
634 | + | sleep 0.5 | |
635 | + | ||
636 | + | # Show the final progress exactly once if showing progress | |
637 | + | if [ "$show_progress" -eq 1 ]; then | |
638 | + | show_final_progress | |
639 | + | fi | |
640 | + | ||
641 | + | # Print summary of failures | |
642 | + | if [ "$quiet" -eq 0 ]; then | |
643 | + | echo -e "\n${BOLD}${PURPLE}$ICON_SUMMARY Processing Summary${NC}" | |
644 | + | fi | |
645 | + | ||
646 | + | # Collect all failure files | |
647 | + | compile_failures=$(find "$compile_failures_dir" -type f | wc -l) | |
648 | + | move_failures=$(find "$move_failures_dir" -type f | wc -l) | |
649 | + | ||
650 | + | if [ "$compile_failures" -eq 0 ] && [ "$move_failures" -eq 0 ]; then | |
651 | + | log_success "All files processed successfully." | |
652 | + | else | |
653 | + | if [ "$compile_failures" -gt 0 ]; then | |
654 | + | echo -e "\n${RED}${BOLD}$ICON_ERROR Compilation failures:${NC}" | |
655 | + | find "$compile_failures_dir" -type f -exec cat {} \; | sort | while read -r failure; do | |
656 | + | echo -e "${RED}- $failure${NC}" | |
657 | + | done | |
658 | + | echo -e "${BLUE}See $compile_log for detailed error messages.${NC}" | |
659 | + | fi | |
660 | + | ||
661 | + | if [ "$move_failures" -gt 0 ]; then | |
662 | + | echo -e "\n${YELLOW}${BOLD}$ICON_ERROR Move failures:${NC}" | |
663 | + | find "$move_failures_dir" -type f -exec cat {} \; | sort | while read -r failure; do | |
664 | + | echo -e "${YELLOW}- $failure${NC}" | |
665 | + | done | |
666 | + | echo -e "${BLUE}See $move_log for detailed error messages.${NC}" | |
667 | + | fi | |
668 | + | fi | |
669 | + | else | |
670 | + | log_warning "No .typ files found to process." | |
671 | + | fi | |
672 | + | ||
673 | + | # Clean up temporary directory and lock files | |
674 | + | rm -rf "$temp_dir" | |
675 | + | rm -f "$progress_lock" "$compile_log.lock" "$move_log.lock" | |
676 | + | ||
677 | + | log_success "Processing complete." |