1#' Install/Uninstall TinyTeX 2#' 3#' The function \code{install_tinytex()} downloads and installs TinyTeX, a 4#' custom LaTeX distribution based on TeX Live. The function 5#' \code{uninstall_tinytex()} removes TinyTeX; \code{reinstall_tinytex()} 6#' reinstalls TinyTeX as well as previously installed LaTeX packages by default; 7#' \code{tinytex_root()} returns the root directory of TinyTeX if found. 8#' @param force Whether to force to install (override) or uninstall TinyTeX. 9#' @param dir The directory to install or uninstall TinyTeX (should not exist 10#' unless \code{force = TRUE}). 11#' @param version The version of TinyTeX, e.g., \code{"2020.09"} (see all 12#' available versions at \url{https://github.com/yihui/tinytex-releases}, or 13#' via \code{xfun::github_releases('yihui/tinytex-releases')}). By default, it 14#' installs the latest daily build of TinyTeX. If \code{version = 'latest'}, 15#' it installs the latest Github release of TinyTeX. 16#' @param repository The CTAN repository to set. You can find available 17#' repositories at \code{https://ctan.org/mirrors}), e.g., 18#' \code{'http://mirrors.tuna.tsinghua.edu.cn/CTAN/'}, or 19#' \code{'https://mirror.las.iastate.edu/tex-archive/'}. In theory, this 20#' argument should end with the path \file{/systems/texlive/tlnet}, and if it 21#' does not, the path will be automatically appended. 22#' @param extra_packages A character vector of extra LaTeX packages to be 23#' installed. By default, a vector of all currently installed LaTeX packages 24#' if an existing installation of TinyTeX is found. If you want a fresh 25#' installation, you may use \code{extra_packages = NULL}. 26#' @param add_path Whether to run the command \command{tlmgr path add} to add 27#' the bin path of TeX Live to the system environment variable \var{PATH}. 28#' @references See the TinyTeX documentation (\url{https://yihui.org/tinytex/}) 29#' for the default installation directories on different platforms. 30#' @export 31install_tinytex = function( 32 force = FALSE, dir = 'auto', version = 'daily', repository = 'ctan', 33 extra_packages = if (is_tinytex()) tl_pkgs(), add_path = TRUE 34) { 35 if (!is.logical(force)) stop('The argument "force" must take a logical value.') 36 check_dir = function(dir) { 37 if (dir_exists(dir) && !force) stop( 38 'The directory "', dir, '" exists. Please either delete it, ', 39 'or use install_tinytex(force = TRUE).' 40 ) 41 } 42 if (missing(dir)) dir = '' 43 user_dir = '' 44 if (dir != '') { 45 dir = gsub('[/\\]+$', '', dir) # remove trailing slashes 46 check_dir(dir) 47 unlink(dir, recursive = TRUE) 48 user_dir = normalizePath(dir, mustWork = FALSE) 49 } 50 51 https = grepl('^https://', repository) 52 repository = normalize_repo(repository) 53 not_ctan = repository != 'ctan' 54 55 owd = setwd(tempdir()); on.exit(setwd(owd), add = TRUE) 56 57 if ((texinput <- Sys.getenv('TEXINPUT')) != '') message( 58 'Your environment variable TEXINPUT is "', texinput, 59 '". Normally you should not set this variable, because it may lead to issues like ', 60 'https://github.com/yihui/tinytex/issues/92.' 61 ) 62 63 switch( 64 os, 65 'unix' = { 66 check_local_bin() 67 if (os_index != 3 && !dir_exists('~/bin')) on.exit(message( 68 'You may have to restart your system after installing TinyTeX to make sure ', 69 '~/bin appears in your PATH variable (https://github.com/yihui/tinytex/issues/16).' 70 ), add = TRUE) 71 }, 72 'windows' = {}, 73 stop('Sorry, but tinytex::install_tinytex() does not support this platform: ', os) 74 ) 75 76 src_install = getOption('tinytex.source.install', need_source_install()) 77 install = function(...) { 78 if (src_install) { 79 install_tinytex_source(repository, ...) 80 } else { 81 install_prebuilt('TinyTeX-1', ..., repo = repository) 82 } 83 } 84 force(extra_packages) # evaluate it before installing another version of TinyTeX 85 if (version == 'daily') { 86 version = '' 87 # test if https://yihui.org or github.com is accessible because the daily 88 # version is downloaded from there 89 determine_version = function() { 90 if (xfun::url_accessible('https://yihui.org')) return('') 91 if (xfun::url_accessible('https://github.com')) return('daily-github') 92 warning( 93 "The daily version of TinyTeX does not appear to be accessible. ", 94 "Switching to version = 'latest' instead. If you are sure to install ", 95 "the daily version, call tinytex::install_tinytex(version = 'daily') ", 96 "(which may fail)." 97 ) 98 'latest' 99 } 100 if (missing(version) && !src_install) version = determine_version() 101 } 102 user_dir = install(user_dir, version, add_path, extra_packages) 103 104 opts = options(tinytex.tlmgr.path = find_tlmgr(user_dir)) 105 on.exit(options(opts), add = TRUE) 106 107 if (not_ctan) { 108 # install tlgpg for Windows and macOS users if an HTTPS repo is preferred 109 if (os_index %in% c(1, 3) && https) { 110 tlmgr(c('--repository', 'http://www.preining.info/tlgpg/', 'install', 'tlgpg')) 111 } 112 tlmgr_repo(repository) 113 if (tlmgr(c('update', '--list')) != 0) { 114 warning('The repository ', repository, ' does not seem to be accessible. Reverting to the default CTAN mirror.') 115 tlmgr(c('option', 'repository', 'ctan')) 116 } 117 } 118 119 invisible(user_dir) 120} 121 122# TinyTeX has to be installed from source for OSes that are not Linux or 123# non-x86_64 Linux machines 124need_source_install = function() { 125 os_index == 0 || (os_index == 2 && !identical(Sys.info()[['machine']], 'x86_64')) 126} 127 128# append /systems/texlive/tlnet to the repo url if necessary 129normalize_repo = function(url) { 130 # don't normalize the url if users passes I(url) or 'ctan' or NULL 131 if (is.null(url) || url == 'ctan' || inherits(url, 'AsIs')) return(url) 132 url = sub('/+$', '', url) 133 if (!grepl('/tlnet$', url)) { 134 url2 = paste0(url, '/systems/texlive/tlnet') 135 # return the amended url if it works 136 if (xfun::url_accessible(url2)) return(url2) 137 } 138 url 139} 140 141win_app_dir = function(..., error = TRUE) { 142 d = Sys.getenv('APPDATA') 143 if (d == '') { 144 if (error) stop('Environment variable "APPDATA" not set.') 145 return(d) 146 } 147 file.path(d, ...) 148} 149 150# check if /usr/local/bin on macOS is writable 151check_local_bin = function() { 152 if (os_index != 3 || is_writable('/usr/local/bin')) return() 153 chown_cmd = 'chown -R `whoami`:admin /usr/local/bin' 154 message( 155 'The directory /usr/local/bin is not writable. I recommend that you ', 156 'make it writable. See https://github.com/yihui/tinytex/issues/24 for more info.' 157 ) 158 if (system(sprintf( 159 "/usr/bin/osascript -e 'do shell script \"%s\" with administrator privileges'", chown_cmd 160 )) != 0) warning( 161 "Please run this command in your Terminal (password required):\n sudo ", 162 chown_cmd, call. = FALSE 163 ) 164} 165 166install_tinytex_source = function(repo = '', dir, version, add_path, extra_packages) { 167 if (version != '') stop( 168 'tinytex::install_tinytex() does not support installing a specific version of ', 169 'TinyTeX for your platform. Please use the argument version = "".' 170 ) 171 if (repo != 'ctan') { 172 Sys.setenv(CTAN_REPO = repo) 173 on.exit(Sys.unsetenv('CTAN_REPO'), add = TRUE) 174 } 175 download_file('https://yihui.org/gh/tinytex/tools/install-unx.sh') 176 res = system2('sh', c( 177 'install-unx.sh', if (repo != 'ctan') c('--no-admin', '--path', shQuote(repo)) 178 )) 179 if (res != 0) stop('Failed to install TinyTeX', call. = FALSE) 180 target = normalizePath(default_inst()) 181 if (!dir_exists(target)) stop('Failed to install TinyTeX.') 182 if (!dir %in% c('', target)) { 183 dir.create(dirname(dir), showWarnings = FALSE, recursive = TRUE) 184 dir_rename(target, dir) 185 target = dir 186 } 187 opts = options(tinytex.tlmgr.path = find_tlmgr(target)) 188 on.exit(options(opts), add = TRUE) 189 post_install_config(add_path, extra_packages, repo) 190 unlink(c('install-unx.sh', 'install-tl.zip', 'pkgs-custom.txt', 'tinytex.profile')) 191 target 192} 193 194os_index = if (is_windows()) 1 else if (is_linux()) 2 else if (is_macos()) 3 else 0 195 196default_inst = function() switch( 197 os_index, win_app_dir('TinyTeX'), '~/.TinyTeX', '~/Library/TinyTeX' 198) 199 200find_tlmgr = function(dir = default_inst()) { 201 bin = file.path(list.files(file.path(dir, 'bin'), full.names = TRUE), 'tlmgr') 202 if (is_windows()) bin = paste0(bin, '.bat') 203 bin[file_test('-x', bin)][1] 204} 205 206#' @rdname install_tinytex 207#' @export 208uninstall_tinytex = function(force = FALSE, dir = tinytex_root()) { 209 tweak_path() 210 if (dir == '') stop('TinyTeX does not seem to be installed.') 211 if (!is_tinytex() && !force) stop( 212 'Detected TeX Live at "', dir, '", but it appears to be TeX Live instead of TinyTeX. ', 213 'To uninstall TeX Live, use the argument force = TRUE.' 214 ) 215 r_texmf('remove', .quiet = TRUE) 216 tlmgr_path('remove') 217 delete_texmf_user() 218 unlink(dir, recursive = TRUE) 219} 220 221# delete user's texmf tree; don't delete ~/.TinyTeX if TinyTeX itself is 222# installed there 223delete_texmf_user = function() { 224 r = dir.exists(d <- path.expand('~/.TinyTeX')) 225 if (!r) return(FALSE) 226 d1 = xfun::normalize_path(tinytex_root(error = FALSE)) 227 if (d1 == '') return() # not TinyTeX 228 d2 = xfun::normalize_path(d) 229 if (substr(d1, 1, nchar(d2)) == d2) return(FALSE) 230 unlink(d, recursive = TRUE) 231 r 232} 233 234#' @param packages Whether to reinstall all currently installed packages. 235#' @param ... Other arguments to be passed to \code{install_tinytex()} (note 236#' that the \code{extra_packages} argument will be set to \code{tl_pkgs()} if 237#' \code{packages = TRUE}). 238#' @rdname install_tinytex 239#' @export 240reinstall_tinytex = function(packages = TRUE, dir = tinytex_root(), ...) { 241 pkgs = if (packages) tl_pkgs() 242 if (length(pkgs)) message( 243 'If reinstallation fails, try install_tinytex() again. Then ', 244 'install the following packages:\n\ntinytex::tlmgr_install(c(', 245 paste('"', pkgs, '"', sep = '', collapse = ', '), '))\n' 246 ) 247 # in theory, users should not touch the texmf-local dir; if they did, I'll try 248 # to preserve it during reinstall: https://github.com/yihui/tinytex/issues/117 249 if (length(list.files(texmf <- file.path(dir, 'texmf-local'), recursive = TRUE)) > 0) { 250 dir.create(texmf_tmp <- tempfile(), recursive = TRUE) 251 message( 252 'The directory ', texmf, ' is not empty. It will be backed up to ', 253 texmf_tmp, ' and restored later.\n' 254 ) 255 file.copy(texmf, texmf_tmp, recursive = TRUE) 256 on.exit( 257 file.copy(file.path(texmf_tmp, basename(texmf)), dirname(texmf), recursive = TRUE), 258 add = TRUE 259 ) 260 } 261 uninstall_tinytex() 262 install_tinytex(extra_packages = pkgs, dir = dir, ...) 263} 264 265#' @param error Whether to signal an error if TinyTeX is not found. 266#' @rdname install_tinytex 267#' @export 268tinytex_root = function(error = TRUE) { 269 tweak_path() 270 path = Sys.which('tlmgr') 271 if (path == '') return('') 272 root_dir = function(path, ...) { 273 dir = normalizePath(file.path(dirname(path), ...), mustWork = TRUE) 274 if (!'bin' %in% list.files(dir)) if (error) stop( 275 dir, ' does not seem to be the root directory of TeX Live (no "bin/" dir under it)' 276 ) else return('') 277 dir 278 } 279 if (os == 'windows') return(root_dir(path, '..', '..')) 280 if (Sys.readlink(path) == '') if (error) stop( 281 'Cannot figure out the root directory of TeX Live from ', path, 282 ' (not a symlink on ', os, ')' 283 ) else return('') 284 path = symlink_root(path) 285 root_dir(normalizePath(path), '..', '..', '..') 286} 287 288# trace a symlink to its final destination 289symlink_root = function(path) { 290 path = normalizePath(path, mustWork = TRUE) 291 path2 = Sys.readlink(path) 292 if (path2 == '') return(path) # no longer a symlink; must be resolved now 293 # path2 may still be a _relative_ symlink 294 in_dir(dirname(path), symlink_root(path2)) 295} 296 297# a helper function to open tlmgr.pl (on *nix) 298open_tlmgr = function() { 299 file.edit(symlink_root(Sys.which('tlmgr'))) 300} 301 302#' Check if the LaTeX installation is TinyTeX 303#' 304#' First find the root directory of the installation via 305#' \code{\link{tinytex_root}()}. Then check if the directory name is 306#' \code{"tinytex"} (case-insensitive). If not, further check if the first line 307#' of the file \file{texmf-dist/web2c/fmtutil.cnf} under the directory contains 308#' \code{"TinyTeX"} or \code{".TinyTeX"}. If the binary version of TinyTeX was 309#' installed, \file{fmtutil.cnf} should contain a line like \samp{Generated by 310#' */TinyTeX/bin/x86_64-darwin/tlmgr on Thu Sep 17 07:13:28 2020}. 311#' @return A logical value indicating if the LaTeX installation is TinyTeX. 312#' @export 313#' @examples tinytex::is_tinytex() 314is_tinytex = function() tryCatch({ 315 root = tinytex_root() 316 root != '' && (gsub('^[.]', '', tolower(basename(root))) == 'tinytex' || any(grepl( 317 '\\W[.]?TinyTeX\\W', 318 readLines(file.path(root, 'texmf-dist/web2c/fmtutil.cnf'), n = 1) 319 ))) 320}, error = function(e) FALSE) 321 322dir_rename = function(from, to) { 323 # cannot rename '/foo' to '/bar' because of 'Invalid cross-device link' 324 suppressWarnings(file.rename(from, to)) || dir_copy(from, to) 325} 326 327dir_copy = function(from, to) { 328 dir.create(to, showWarnings = FALSE, recursive = TRUE) 329 all(file.copy(list.files(from, full.names = TRUE), to, recursive = TRUE)) && 330 unlink(from, recursive = TRUE) == 0 331} 332 333download_file = function(...) { 334 xfun::download_file(..., quiet = Sys.getenv('APPVEYOR') != '') 335} 336 337# LaTeX packages that I use 338install_yihui_pkgs = function() { 339 pkgs = readLines('https://yihui.org/gh/tinytex/tools/pkgs-yihui.txt') 340 tlmgr_install(pkgs) 341} 342 343# install a prebuilt version of TinyTeX 344install_prebuilt = function( 345 pkg = '', dir = '', version = '', add_path = TRUE, extra_packages = NULL, 346 repo = 'ctan', hash = FALSE, cache = NA 347) { 348 if (need_source_install()) stop( 349 'There is no prebuilt version of TinyTeX for this platform: ', 350 paste(Sys.info()[c('sysname', 'machine')], collapse = ' '), '.' 351 ) 352 dir0 = default_inst(); b = basename(dir0) 353 dir1 = xfun::normalize_path(dir) # expected installation dir 354 if (dir1 == '') dir1 = dir0 355 # the archive is extracted to this target dir 356 target = dirname(dir1) 357 dir2 = file.path(target, b) # path to (.)TinyTeX/ after extraction 358 359 if (xfun::file_ext(pkg) == '') { 360 if (version == 'latest') { 361 version = xfun::github_releases('yihui/tinytex-releases', version) 362 } else if (version == 'daily-github') { 363 version = '' 364 opts = options(tinytex.install.url = 'https://github.com/yihui/tinytex-releases/releases/download/daily/') 365 on.exit(options(opts), add = TRUE) 366 } 367 version = gsub('^v', '', version) 368 installer = if (pkg == '') 'TinyTeX' else pkg 369 # e.g., TinyTeX-0.zip, TinyTeX-1-v2020.10.tar.gz, ... 370 pkg = paste0( 371 installer, if (version != '') paste0('-v', version), '.', 372 c('zip', 'tar.gz', 'tgz')[os_index] 373 ) 374 if (file.exists(pkg) && is.na(cache)) { 375 # invalidate cache (if unspecified) when the installer is more than one day old 376 if (as.numeric(difftime(Sys.time(), file.mtime(pkg), units = 'days')) > 1) 377 cache = FALSE 378 } 379 if (xfun::isFALSE(cache)) { 380 file.remove(pkg); on.exit(file.remove(pkg), add = TRUE) 381 } 382 if (!file.exists(pkg)) download_installer(pkg, version) 383 } 384 pkg = path.expand(pkg) 385 386 # installation dir shouldn't be a file but a directory 387 file.remove(exist_files(c(dir1, dir2))) 388 extract = if (grepl('[.]zip$', pkg)) unzip else untar 389 extract(pkg, exdir = path.expand(target)) 390 # TinyTeX (or .TinyTeX) is extracted to the parent dir of `dir`; may need to rename 391 if (dir != '') { 392 if (basename(dir1) != b) file.rename(dir2, dir1) 393 opts = options(tinytex.tlmgr.path = find_tlmgr(dir1)) 394 on.exit(options(opts), add = TRUE) 395 } 396 post_install_config(add_path, extra_packages, repo, hash) 397 invisible(dir1) 398} 399 400# post-install configurations 401post_install_config = function(add_path, extra_packages, repo, hash = FALSE) { 402 if (os_index == 2) { 403 dir.create('~/bin', FALSE, TRUE) 404 tlmgr(c('option', 'sys_bin', '~/bin')) 405 } 406 # fix fonts.conf: https://github.com/yihui/tinytex/issues/313 407 tlmgr(c('postaction', 'install', 'script', 'xetex'), .quiet = TRUE) 408 # do not wrap lines in latex log (#322) 409 tlmgr_conf(c('texmf', 'max_print_line', '10000'), .quiet = TRUE, stdout = FALSE) 410 411 if (add_path) tlmgr_path() 412 r_texmf(.quiet = TRUE) 413 # don't use the default random ctan mirror when installing on CI servers 414 if (repo != 'ctan' || tolower(Sys.getenv('CI')) != 'true') 415 tlmgr_repo(repo, stdout = FALSE, .quiet = TRUE) 416 tlmgr_install(setdiff(extra_packages, tl_pkgs())) 417 if (hash) { 418 texhash(); fmtutil(stdout = FALSE); updmap(); fc_cache() 419 } 420} 421 422download_installer = function(file, version) { 423 url = if (version != '') sprintf( 424 'https://github.com/yihui/tinytex-releases/releases/download/v%s/%s', version, file 425 ) else paste0(getOption('tinytex.install.url', 'https://yihui.org/tinytex/'), file) 426 download_file(url, file) 427} 428 429#' Copy TinyTeX to another location and use it in another system 430#' 431#' The function \code{copy_tinytex()} copies the existing TinyTeX installation 432#' to another directory (e.g., a portable device like a USB stick). The function 433#' \code{use_tinytex()} runs \command{tlmgr path add} to add the copy of TinyTeX 434#' in an existing folder to the \code{PATH} variable of the current system, so 435#' that you can use utilities such as \command{tlmgr} and \command{pdflatex}, 436#' etc. 437#' @param from The root directory of the TinyTeX installation. For 438#' \code{copy_tinytex()}, the default value \code{tinytex_root()} should be a 439#' reasonable guess if you installed TinyTeX via \code{install_tinytex()}. For 440#' \code{use_tinytex()}, if \code{from} is not provided, a dialog for choosing 441#' the directory interactively will pop up. 442#' @param to The destination directory where you want to make a copy of TinyTeX. 443#' Like \code{from} in \code{use_tinytex()}, a dialog will pop up if \code{to} 444#' is not provided in \code{copy_tinytex()}. 445#' @note You can only copy TinyTeX and use it in the same system, e.g., the 446#' Windows version of TinyTeX only works on Windows. 447#' @export 448copy_tinytex = function(from = tinytex_root(), to = select_dir('Select Destination Directory')) { 449 if (!dir_exists(from)) stop('TinyTeX does not seem to be installed.') 450 if (length(to) != 1 || !dir_exists(to)) 451 stop("The destination directory '", to, "' does not exist.") 452 file.copy(from, to, recursive = TRUE) 453} 454 455#' @rdname copy_tinytex 456#' @export 457use_tinytex = function(from = select_dir('Select TinyTeX Directory')) { 458 if (length(from) != 1) stop('Please provide a valid path to the TinyTeX directory.') 459 d = list.files(file.path(from, 'bin'), full.names = TRUE) 460 d = d[dir_exists(d)] 461 if (length(d) != 1) stop("The directory '", from, "' does not contain TinyTeX.") 462 p = file.path(d, 'tlmgr') 463 if (os == 'windows') p = paste0(p, '.bat') 464 if (system2(p, c('path', 'add')) != 0) stop( 465 "Failed to add '", d, "' to your system's environment variable PATH. You may ", 466 "consider the fallback approach, i.e., set options(tinytex.tlmgr.path = '", p, "')." 467 ) 468 message('Restart R and your editor and check if tinytex::tinytex_root() points to ', from) 469} 470 471select_dir = function(caption = 'Select Directory') { 472 d = tryCatch(rstudioapi::selectDirectory(caption), error = function(e) { 473 if (os == 'windows') utils::choose.dir(caption = caption) else { 474 tcltk::tk_choose.dir(caption = caption) 475 } 476 }) 477 if (!is.null(d) && !is.na(d)) d 478} 479