--[===================================================================[-- Script and program wrappers in TeX Live for Windows License Public Domain Originally written in 2009 by Tomasz M. Trzeciak. Prior work: 'tl-w32-wrapper.texlua' by Reinhard Kotucha and Norbert Preining. 'tl-w32-wrapper.cmd' by Tomasz M. Trzeciak. Rationale Wrappers enable to use scripts on Windows as regular programs. They are also required for some binary programs to set-up the right environment for them. Batch scripts can be used for wrapping but they are not as universal as binaries (there are some odd cases where they don't work) and it is hard to make them robust and secure. Compiled binary wrappers don't suffer from these problems but they are harder to write, debug and maintain in comparison to scripts. For these reasons a hybrid approach was adopted that offers the best of both worlds - a binary stub combined with a launcher script. Source files runscript.tlu launcher script for locating and dispatching target scripts/programs runscript_dll.c common DLL part of the binary stubs; locates and calls the launcher script runscript_exe.c EXE proxy to the common DLL for CLI mode stubs wrunscript_exe.c EXE proxy to the common DLL for GUI mode stubs Compilation of binaries (requires luatex.dll in the same directory) with gcc (size optimized): gcc -Os -s -shared -o runscript.dll runscript_dll.c -L./ -lluatex gcc -Os -s -o runscript.exe runscript_exe.c -L./ -lrunscript gcc -mwindows -Os -s -o wrunscript.exe wrunscript_exe.c -L./ -lrunscript with tcc (extra small size): tiny_impdef luatex.dll tcc -shared -o runscript.dll runscript_dll.c luatex.def tcc -o runscript.exe runscript_exe.c runscript.def tcc -o wrunscript.exe wrunscript_exe.c runscript.def Structure of the wrapper Wrappers consist of small binary stubs and a common texlua script. The binary stubs are all the same, just different names (but CLI and GUI stubs differ, see below, and GUI stubs are actually all different due to different embedded icons). The job of the binary stub is twofold: (a) call the texlua launcher script 'runscript.tlu' from the same directory (or more precisely from the directory containing 'runscript.dll') and (b) pass to it argv[0] and the unparsed argument string as the last two arguments (after adding a sentinel argument, which ends with a new line character). Arbitrary C strings can be passed, because the script is executed by linking with luatex.dll and calling its lua interpreter directly rather than by spawning a new process. There are two flavours of the binary stub: one for CLI programs and another one for GUI programs. The GUI variant does not open a console window nor does it block the command promt if started from there. It also uses a dialog box to display an error message in addition to outputting to stderr. The stubs are further split into a common DLL and EXE proxies to it. This is for maintenance reasons - updates can be done by replacement of a single DLL rather than all binary stubs. The launcher script knows, which variant has been used to invoke it based on the sentinel argument. The lack of this argument means that it was invoked in a standard way, i.e. through texlua.exe. All the hard work of locating a script/program to execute happens in the launcher script. The located script/program is always executed directly by spawning its interpreter (or binary) in a new process. The system shell (cmd.exe) is never called (except for batch scripts, of course). If the located script happens to be a (tex)lua script, it is loaded and called directly from within this script, i.e. no new process is spawned. Adding wrappers for user scripts The script wrapping machinery is not limited to scripts included in TeX Live. You can also use it for script programs from manually installed packages, which should minimize the problems when using them with TeX Live. First, make sure that there is an interpreter program available on your system for the script you want to use. Interpreters for Perl and Lua are bundled with TeX Live, all others have to be installed independently. The following script types and their file extensions are currently supported and searched in that order: Lua (.tlu;.texlua;.lua) -- included Perl (.pl) -- included Ruby (.rb) -- requires installation Python (.py) -- requires installation Java (.jar) -- requires installation VBScript (.vbs) -- part of Windows Jscript (.js) -- part of Windows Batch (.bat;.cmd) -- part of Windows For Unix-style extensionless scripts their first line starting with she-bang (#!) is consulted for the interpreter program to run. This program must be present on the search path. Next, the script program needs to be installed somewhere below the 'scripts' directory under one of the TEXMF trees (consult the documentation or texmf/web2c/texmf.cnf file for a list). You may need to update the file search database afterwards with: mktexlsr [TEXMFDIR] Test if the script can be located with: kpsewhich --format=texmfscripts . Once installed the script can be called immediately with: runscript [script arguments] If you prefer to call the script program simply by its name, copy and rename bin/win32/runscript.exe to .exe and put it in bin/win32/ directory of your TeX Live installation or, if you don't have the write permissions there, somewhere else on the search path. Changelog: 2009/12/04 - initial version 2009/12/15 - minor fixes for path & extension list parsing 2010/01/09 - add support for GUI mode stubs 2010/02/28 - enable GUI mode stubs for dviout, psv and texworks; - added generic handling of sys programs - added restricted repstopdf to alias_table 2010/03/13 - added 'readme.txt' and changelog - added support and docs for calling user added scripts; (use path of 'runscript.dll' instead of .exe stub to locate 'runscript.tlu' script) - limit search for shell_escape_commands to system trees - added function for creating directory hierarchy - fixed directory creation for dviout & texworks aliases - fixed arg[0] of repstopdf & rpdfcrop --]===================================================================]-- -- HELPER SUBROUTINES -- -- quotes string with spaces local function _q(str) str = string.gsub(str, '"', '') -- disallow embedded double quotes return string.find(str, "%s") and '"'..str..'"' or str end -- checks if path is absolute (but not if it actually exists) local function is_abs_path(fpath) return string.find(fpath, '^[a-zA-Z]:[/\\]') and true or false end -- prepends directories to path if they are not already there local function prepend_path(path, ...) local pathcmp = string.lower(string.gsub(path, '/', '\\'))..';' for k = 1, select('#', ...) do local dir = string.lower(string.gsub(select(k, ...), '/', '\\'))..';' if not string.find(pathcmp, dir, 1, true) then path = dir..path end end return path end -- searches the PATH for a file local function search_path(fname, PATH, PATHEXT) if string.find(fname, '[/\\]') then return nil, "directory part not allowed for PATH search: "..fname end PATH = PATH or os.getenv('PATH') PATHEXT = PATHEXT or '\0' -- '\0' for no extension for dir in string.gmatch(PATH, '[^;]+') do local dirsep = (string.find(dir, '\\') and '\\' or '/') for ext in string.gmatch(PATHEXT, '[^;]+') do local f = dir..dirsep..fname..ext if lfs.isfile(f) then return f, ext end end end return nil, "file not on PATH: "..fname end -- locates texmfscript to execute local function find_texmfscript(progname, ext_list) for ext in string.gmatch(ext_list, '[^;]+') do local progfullname = kpse.find_file(progname..ext, 'texmfscripts') if progfullname then return progfullname, ext end end return nil, "no appropriate script or program found: "..progname end -- converts the #! line to arg table -- used for scripts w/o extension -- only the two most common cases are considered: -- #! /path/to/command [options] -- #! /usr/bin/env command [options] -- ([options] after the command are retained as well) local function shebang_to_argv(progfullname) local fid, errmsg = io.open(progfullname, 'r') if not fid then return nil, errmsg end local fstln = fid:read('*line') fid:close() if (string.sub(fstln, 1, 2) ~= '#!') then return nil, "don't know how to execute script: "..progfullname end local argv = string.explode( string.sub(fstln, 3) ) -- split on spaces argv[1] = string.match(argv[1], '[^/]+$') if (argv[1] == 'env') then table.remove(argv, 1) end return argv end -- checks if command exist on the path and returns it local function check_command(cmd, PATH) local cmdext = cmd..(string.find(cmd, '%.[^\\/.]*$') and '' or '.exe') local fullcmd = is_abs_path(cmdext) and lfs.isfile(cmdext) and cmdext or search_path(cmdext, PATH) if fullcmd then return fullcmd else return nil, 'program not found (not part of TeX Live): '..cmdext end end -- creates directory or directory hierarchy local function mkdir_plus(dir) if lfs.mkdir(dir) then return true end -- try with system's mkdir in case we need to create intermediate dirs too local ret = os.spawn({[0]=search_path("cmd.exe"), string.format('cmd.exe /x /c mkdir "%s"', dir)}) if ret == 0 then return true else return nil, "cannot create directory: " .. dir end end -- MAIN CHUNK -- -- localize the assert function (it will be replaced in gui mode) local assert = assert local guimode = false local argline = '' -- check for the sentinel argment coming from the .exe stub if arg[#arg-2] and ( string.sub(arg[#arg-2], -1) == '\n' ) then -- argv[0] and unparsed argument line are passed -- from the .exe stub as the two last arguments -- pop them up from the arg table argline = table.remove(arg) -- pop unparsed arguments arg[0] = table.remove(arg) -- pop C stub's argv[0] guimode = (table.remove(arg) == 'GUI_MODE\n') -- pop sentinel argument if guimode then -- replace the assert function, if we are running w/o console window function assert(...) if select(1, ...) then return ... end local error_msg = select(2, ...) if type(error_msg) ~= 'string' then error_msg = "assertion failed!" end -- store the error message in an env var and throw an error -- it will be catched on the C side at exit and displayed -- in a message box os.setenv('RUNSCRIPT_ERROR_MESSAGE', error_msg) error(error_msg, 2) end end else -- we must be called as: texlua runscript.tlu progname ... -- this is treated the same as: runscript[.exe] progname ... -- we don't have the unparsed arument line in this case, so construct one for k = 1, #arg do argline = argline..' '.._q(arg[k]) end end -- ( lower arg[0] : get file name part : remove extension ) == 'runscript' if (string.lower(arg[0]):match('[^\\/]+$'):gsub('%.[^.]*$', '') == 'runscript') then -- we are called as: runscript progname ... -- remove the first argument from the arg table and from the argline string -- (this argument should have no embedded spaces!) arg[0] = table.remove(arg, 1) assert(arg[0], "not enough arguments!\nusage: runscript program-name [arguments]") argline = string.gsub(argline, '^%S+%s*', '') end -- program name local progname, k = string.lower(arg[0]):match('[^\\/]+$'):gsub('%.[^.]*$', ''):gsub('%-sys$', '') local sysprog = (k > 0) -- sys type program/script (there was -sys suffix removed) if (progname == 'runscript') then -- prevent recursive calls to this script assert(nil, "oops! wrapping the wrapper?") end -- init kpathsea local k = -1 while arg[k-1] do k = k - 1 end -- in case of a call: luatex --luaonly ... local lua_binary = arg[k] kpse.set_program_name(lua_binary, progname) -- various dir-vars local TEXDIR = kpse.var_value('SELFAUTOPARENT') local BINDIR = kpse.var_value('SELFAUTOLOC') local PATH = os.getenv('PATH') or '' -- perl stuff local scripts4tlperl = { ['updmap-sys'] = true, updmap = true, } local PERLEXE = search_path('perl.exe', PATH) if not PERLEXE or guimode or scripts4tlperl[progname] then PERLEXE = TEXDIR..'/tlpkg/tlperl/bin/perl.exe' os.setenv('PERL5LIB', TEXDIR..'/tlpkg/tlperl/lib')--[[ local PERL5SHELL = os.getenv('COMSPEC') PERL5SHELL = string.gsub(PERL5SHELL, '\\', '\\\\') PERL5SHELL = string.gsub(PERL5SHELL, ' ', '\\ ') os.setenv('PERL5SHELL', PERL5SHELL..' /x /c')--]] PATH = prepend_path(PATH, TEXDIR..'/tlpkg/tlperl/bin') end -- gs stuff os.setenv('GS_LIB', TEXDIR..'/tlpkg/tlgs/lib;'..TEXDIR..'/tlpkg/tlgs/fonts') os.setenv('GS_DLL', TEXDIR..'/tlpkg/tlgs/bin/gsdll32.dll') -- path PATH = prepend_path(PATH, TEXDIR..'/tlpkg/tlgs/bin', BINDIR) os.setenv('PATH', PATH); -- sys stuff if sysprog then os.setenv('TEXMFVAR', kpse.var_value('TEXMFSYSVAR')) os.setenv('TEXMFCONFIG', kpse.var_value('TEXMFSYSCONFIG')) end -- restricted programs local shell_escape_commands = string.lower(kpse.var_value('shell_escape_commands')) if string.find(','..shell_escape_commands..',', ','..progname..',', 1, true) then -- limit search path to the system trees (not really necessary for the names in -- the alias_table, see below, because they are not searched for with kpathsea) os.setenv('TEXMFSCRIPTS', '{!!$TEXMFMAIN,!!$TEXMFLOCAL,!!$TEXMFDIST}/scripts/{$engine,$progname,}//') end -- program alias table (programs with special needs) local alias_table = {--[[ ['fmtutil-sys'] = {[0]=BINDIR..'/fmtutil.exe', 'fmtutil-sys', argline}, ['updmap-sys'] = function () os.setenv('TEXMFVAR', kpse.var_value('TEXMFSYSVAR')) os.setenv('TEXMFCONFIG', kpse.var_value('TEXMFSYSCONFIG')) return {[0]=PERLEXE, 'perl', _q(TEXDIR..'/texmf/scripts/tetex/updmap.pl'), argline} end,--]] asy = function () -- TODO: check if ASYMPTOTE_GS requires quoting of names with spaces --os.setenv('ASYMPTOTE_GS', _q(TEXDIR..'/tlpkg/tlgs/bin/gswin32c.exe')) os.setenv('ASYMPTOTE_GS', 'gswin32c.exe') os.setenv('CYGWIN', 'nodosfilewarning') return {[0]=TEXDIR..'/tlpkg/asymptote/asy.exe', 'asy', argline} end, --dvigif = {[0]=BINDIR..'/dvipng.exe', 'dvigif', argline}, dviout = function () local fontsdir = kpse.var_value('TEXMFVAR') .. '/fonts' if lfs.attributes(fontsdir, 'mode') ~= 'directory' then assert(mkdir_plus(fontsdir)) end local tfmpath = kpse.show_path('tfm') tfmpath = string.gsub(tfmpath, '!!', '') tfmpath = string.gsub(tfmpath, '/', '\\') local texrt = {} for d in string.gmatch(tfmpath, '([^;]+\\fonts)\\tfm[^;]*') do if lfs.attributes(d, 'mode') == 'directory' then table.insert(texrt, d) end end local dvioutpar = [[-NULL -dpi=600 "-gen=']] .. string.gsub(TEXDIR, '/', '\\') .. [[\tlpkg\dviout\gen_pk'" ]] .. [["-TEXPK=^r\pk\\^s.^dpk;^r\tfm\\^s^tfm;^r\vf\\^s.vf;^r\ovf\\^s.ovf;^r\tfm\\^s.tfm" "-TEXROOT=']] .. table.concat(texrt, ';') .. [['"]] return {[0]=TEXDIR..'/tlpkg/dviout/dviout.exe', 'dviout', dvioutpar, argline} end, psv = {[0]=TEXDIR..'/tlpkg/tlpsv/gswxlua.exe', 'gswxlua', '-l', _q(TEXDIR..'/tlpkg/tlpsv/psv.wx.lua'), '-p', _q(TEXDIR..'/tlpkg/tlpsv/psv_view.ps'), '-sINPUT='..argline}, repstopdf = {[0]=PERLEXE, 'perl', _q(TEXDIR..'/texmf-dist/scripts/epstopdf/epstopdf.pl'), '--restricted', argline}, rpdfcrop = {[0]=PERLEXE, 'perl', _q(TEXDIR..'/texmf-dist/scripts/pdfcrop/pdfcrop.pl'), '--restricted', argline}, sam2p = {[0]=TEXDIR..'/tlpkg/sam2p/sam2p.exe', 'sam2p', argline}, texworks = function () local TW_LIBPATH = kpse.var_value('TW_LIBPATH') or kpse.var_value('TEXMFCONFIG')..'/texworks' local TW_INIPATH = kpse.var_value('TW_INIPATH') or TW_LIBPATH os.setenv('TW_LIBPATH', TW_LIBPATH) os.setenv('TW_INIPATH', TW_INIPATH) if (TW_INIPATH and lfs.attributes(TW_INIPATH, 'mode') ~= 'directory') then assert(mkdir_plus(TW_INIPATH)) end return {[0]=TEXDIR..'/tlpkg/texworks/texworks.exe', 'texworks', argline} end, } local extension_map = { -- maps script extension to command ['.bat'] = {'cmd', '/c', 'call'}, ['.cmd'] = {'cmd', '/c', 'call'}, ['.jar'] = {guimode and 'javaw' or 'java', '-jar'}, ['.js'] = {guimode and 'wscript' or 'cscript', '-nologo'}, ['.pl'] = {guimode and 'wperl' or 'perl'}, ['.py'] = {guimode and 'pythonw' or 'python'}, ['.rb'] = {guimode and 'rubyw' or 'ruby'}, ['.vbs'] = {guimode and 'wscript' or 'cscript', '-nologo'}, } local program = alias_table[progname] if program then -- special case (alias) if (type(program) == 'function') then program = program() end else -- general case (no alias) local progfullname, ext = search_path(progname, BINDIR, ".tlu;.bat;.cmd") if not progfullname then progfullname, ext = assert(find_texmfscript(progname, ".tlu;.texlua;.lua;.pl;.rb;.py;.jar;.vbs;.js;.bat;.cmd;\0")) end if (ext == '.lua') or (ext == '.tlu') or (ext == '.texlua') then -- lua script arg[0] = progfullname program = progfullname else program = extension_map[ext] or assert(shebang_to_argv(progfullname)) table.insert(program, _q(progfullname)) table.insert(program, argline) program[0] = program[0] or assert(check_command(program[1], PATH)) end end local atype = type(program) if (atype == 'table') then os.exit(assert(os.spawn(program))) elseif (atype == 'string') then program = assert(loadfile(program)) program() else assert(nil, "unexpected argument type: "..atype) end