From c83af72b159cac683db76f3e9735dd2ab0c309ec Mon Sep 17 00:00:00 2001 From: teatimeguest Date: Sat, 27 Nov 2021 16:00:46 +0900 Subject: [PATCH] feat: add support for legacy versions (#13) --- .github/workflows/check-all-versions.yml | 95 +++++ .github/workflows/ci.yml | 10 - README.md | 23 +- __tests__/setup.test.ts | 306 +++++++-------- __tests__/texlive.test.ts | 480 ++++++++++++----------- action.yml | 3 +- dist/index.js | 274 ++++++++----- src/setup.ts | 18 +- src/texlive.ts | 380 ++++++++++++------ 9 files changed, 941 insertions(+), 648 deletions(-) create mode 100644 .github/workflows/check-all-versions.yml diff --git a/.github/workflows/check-all-versions.yml b/.github/workflows/check-all-versions.yml new file mode 100644 index 0000000..7e8ee6e --- /dev/null +++ b/.github/workflows/check-all-versions.yml @@ -0,0 +1,95 @@ +name: Check all versions + +on: workflow_dispatch + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + version: + - 2008 + - 2009 + - 2010 + - 2011 + - 2012 + - 2013 + - 2014 + - 2015 + - 2016 + - 2017 + - 2018 + - 2019 + - 2020 + - 2021 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install TeX Live + uses: ./ + with: + version: ${{ matrix.version }} + cache: false + + - run: tlmgr --version + + windows: + runs-on: windows-latest + strategy: + matrix: + version: + - 2008 + - 2009 + - 2010 + - 2011 + - 2012 + - 2013 + - 2014 + - 2015 + - 2016 + - 2017 + - 2018 + - 2019 + - 2020 + - 2021 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install TeX Live + uses: ./ + with: + version: ${{ matrix.version }} + cache: false + + - run: tlmgr --version + + macos: + runs-on: macos-latest + strategy: + matrix: + version: + - 2013 + - 2014 + - 2015 + - 2016 + - 2017 + - 2018 + - 2019 + - 2020 + - 2021 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install TeX Live + uses: ./ + with: + version: ${{ matrix.version }} + cache: false + + - run: tlmgr --version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bfc1c5..6bf0153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,25 +31,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - version: ['2019', 'latest'] fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: 12 - - - run: npm ci - - run: npm run build - - name: Install TeX Live uses: ./ with: - version: ${{ matrix.version }} cache: false - run: tlmgr --version diff --git a/README.md b/README.md index c92ad28..eda6197 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Linux, Windows, and macOS are supported. ## Usage -Installing the latest version of TeX Live with `scheme-infraonly`: +Installing the latest version of TeX Live: ```yaml - name: Setup TeX Live @@ -21,8 +21,10 @@ Installing the latest version of TeX Live with `scheme-infraonly`: run: tlmgr --version ``` -Additional TeX packages to be installed -can be specified by the `packages` input: +The action installs TeX Live with +`scheme-infraonly` for `2016` or later versions, and +`scheme-minimal` for the other versions. +If you want to install additional packages, you can use the `packages` input: ```yaml - name: Setup TeX Live @@ -34,17 +36,17 @@ can be specified by the `packages` input: hyperref ``` -An old version of TeX Live is also available: +A legacy version of TeX Live is also available: ```yaml -- name: Setup TeX Live 2019 +- name: Setup TeX Live 2008 uses: teatimeguest/setup-texlive-action@v1 with: - version: 2019 + version: 2008 ``` -Versions prior to `2019` are currently not supported -as the `install-tl` script does not work properly on Windows and macOS. +Supported versions are `2008` to `2021` for Linux and Windows, and +`2013` to `2021` for macOS. ## Caching @@ -59,6 +61,9 @@ If you want to disable caching, you can use the `cache` input: cache: false ``` +The value of the `packages` input is hashed and becomes part of the cache key, +so it affects which cache is restored. + ## Inputs All inputs are optional. @@ -68,7 +73,7 @@ All inputs are optional. |`cache`|Bool|Enable caching for `TEXDIR`. The default is `true`.| |`packages`|String|Whitespace-separated list of TeX packages to install. Schemes and collections can also be specified.| |`prefix`|String|TeX Live installation prefix. The default is `C:\TEMP\setup-texlive` on Windows, `/tmp/setup-texlive` on Linux and macOS.| -|`version`|String|Version of TeX Live to install. Supported values are `2019`, `2020`, `2021`, and `latest`.| +|`version`|String|Version of TeX Live to install. Supported values are `2008` to `2021`, and `latest`.| ## Outputs diff --git a/__tests__/setup.test.ts b/__tests__/setup.test.ts index 6f8ecee..2865998 100644 --- a/__tests__/setup.test.ts +++ b/__tests__/setup.test.ts @@ -7,35 +7,10 @@ import * as core from '@actions/core'; import * as setup from '#/setup'; import * as tl from '#/texlive'; -jest.mock('os', () => ({ - arch: jest.requireActual('os').arch, - platform: jest.fn(), -})); -jest.mock('path', () => { - const actual = jest.requireActual('path'); - return { - join: jest.fn(), - posix: actual.posix, - win32: actual.win32, - }; -}); -jest.spyOn(cache, 'restoreCache'); -jest.spyOn(cache, 'saveCache'); -jest.spyOn(core, 'getBooleanInput'); -jest.spyOn(core, 'getInput'); -jest.spyOn(core, 'getState'); -jest.spyOn(core, 'saveState'); -jest.spyOn(core, 'setOutput'); -jest.spyOn(tl, 'install'); -jest.spyOn(tl.Manager.prototype, 'install'); -jest.spyOn(tl.Manager.prototype, 'pathAdd'); +const random = (): string => (Math.random() + 1).toString(32).substring(7); const env = { ...process.env }; -const notIf = (condition: boolean) => { - return (x: jest.JestMatchers) => (condition ? x : x.not); -}; - let context: { inputs: { cache: boolean; @@ -49,12 +24,54 @@ let context: { }; }; +jest.mock('os', () => ({ + arch: jest.requireActual('os').arch, + platform: jest.fn(), +})); +jest.mock('path', () => { + const actual = jest.requireActual('path'); + return { + join: jest.fn(), + posix: actual.posix, + win32: actual.win32, + }; +}); +jest.spyOn(cache, 'restoreCache').mockResolvedValue(''); +jest.spyOn(cache, 'saveCache').mockImplementation(); +jest.spyOn(core, 'getBooleanInput').mockImplementation((name) => { + if (name === 'cache') { + return context.inputs.cache; + } + throw new Error(`Unexpected argument: ${name}`); +}); +jest.spyOn(core, 'getInput').mockImplementation((name) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (context.inputs as any)[name]; + if (typeof value === 'string') { + return value; + } + throw new Error(`Unexpected argument: ${name}`); +}); +jest.spyOn(core, 'getState').mockImplementation((name) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (context.state as any)[name]; + if (typeof value === 'string') { + return value; + } + throw new Error(`Unexpected argument: ${name}`); +}); +jest.spyOn(core, 'saveState').mockImplementation(); +jest.spyOn(core, 'setOutput').mockImplementation(); +jest.spyOn(tl, 'install').mockImplementation(); +jest.spyOn(tl.Manager.prototype, 'install').mockImplementation(); +jest.spyOn(tl.Manager.prototype, 'pathAdd').mockImplementation(); + beforeEach(() => { console.log('::stop-commands::stoptoken'); process.env = { ...env }; process.env['GITHUB_PATH'] = ''; - process.env['ACTIONS_CACHE_URL'] = ''; + process.env['ACTIONS_CACHE_URL'] = random(); context = { inputs: { @@ -71,39 +88,9 @@ beforeEach(() => { (os.platform as jest.Mock).mockReturnValue('linux'); (path.join as jest.Mock).mockImplementation(path.posix.join); - (cache.saveCache as jest.Mock).mockImplementation(); - (cache.restoreCache as jest.Mock).mockResolvedValue(''); - (core.getBooleanInput as jest.Mock).mockImplementation((name) => { - if (name === 'cache') { - return context.inputs.cache; - } - throw new Error(`Unexpected argument: ${name}`); - }); - (core.getInput as jest.Mock).mockImplementation((name) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value = (context.inputs as any)[name]; - if (typeof value === 'string') { - return value; - } - throw new Error(`Unexpected argument: ${name}`); - }); - (core.getState as jest.Mock).mockImplementation((name) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value = (context.state as any)[name]; - if (typeof value === 'string') { - return value; - } - throw new Error(`Unexpected argument: ${name}`); - }); - (core.saveState as jest.Mock).mockImplementation(); - (core.setOutput as jest.Mock).mockImplementation(); - (tl.install as jest.Mock).mockImplementation(); - (tl.Manager.prototype.install as jest.Mock).mockImplementation(); - (tl.Manager.prototype.pathAdd as jest.Mock).mockImplementation(); }); afterEach(() => { - jest.resetAllMocks(); jest.clearAllMocks(); }); @@ -111,134 +98,107 @@ afterAll(async () => { console.log('::stoptoken::'); }, 100000); -describe('setup()', () => { - describe.each([ - [ - { - cache: true, - packages: '', - prefix: '', - version: 'latest', - }, - 'linux', - { - packages: [], - prefix: '/tmp/setup-texlive', - version: '2021', - }, - ], - [ - { - cache: false, - packages: 'foo bar\n baz', - prefix: '/usr/local/texlive', - version: '2019', - }, - 'darwin', - { - packages: ['bar', 'baz', 'foo'], - prefix: '/usr/local/texlive', - version: '2019', - }, - ], - [ - { - cache: true, - packages: '', - prefix: '', - version: 'latest', - }, - 'win32', - { - packages: [], - prefix: 'C:\\TEMP\\setup-texlive', - version: '2021', - }, - ], - [ - { - cache: false, - packages: 'foo bar\n baz', - prefix: 'C:\\texlive', - version: '2019', - }, - 'win32', - { - packages: ['bar', 'baz', 'foo'], - prefix: 'C:\\texlive', - version: '2019', - }, - ], - ])('with %p on %s', (inputs, platform, data) => { - beforeEach(() => { - (os.platform as jest.Mock).mockReturnValue(platform); - (path.join as jest.Mock).mockImplementation( - platform === 'win32' ? path.win32.join : path.posix.join, - ); - context.inputs = inputs; - }); - - test('without cache', async () => { - await setup.run(); - - notIf(inputs.cache)(expect(cache.restoreCache)).toBeCalled(); - expect(tl.install).toBeCalledWith(data.version, data.prefix, platform); - expect(tl.Manager.prototype.install).toBeCalledWith(data.packages); - notIf(inputs.cache)(expect(core.saveState)).toBeCalledWith( - 'key', - expect.anything(), - ); - expect(core.saveState).toBeCalledWith('post', true); - expect(core.setOutput).toBeCalledWith('cache-hit', false); - }); +describe('setup', () => { + it('sets up TeX Live by the default settings on Linux', async () => { + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).toBeCalledWith('2021', '/tmp/setup-texlive', 'linux'); + expect(tl.Manager.prototype.install).toBeCalledWith([]); + expect(core.saveState).toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', false); + }); - test('with system cache', async () => { - (cache.restoreCache as jest.Mock).mockImplementation( - async (paths, primaryKey, restoreKeys) => restoreKeys?.[0] ?? '', - ); + it('sets up TeX Live by the default settings on Windows', async () => { + (os.platform as jest.Mock).mockReturnValue('win32'); + (path.join as jest.Mock).mockImplementation(path.win32.join); + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).toBeCalledWith( + '2021', + 'C:\\TEMP\\setup-texlive', + 'win32', + ); + expect(tl.Manager.prototype.install).toBeCalledWith([]); + expect(core.saveState).toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', false); + }); - await setup.run(); + it('sets up TeX Live by the default settings on macOS', async () => { + (os.platform as jest.Mock).mockReturnValue('darwin'); + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).toBeCalledWith('2021', '/tmp/setup-texlive', 'darwin'); + expect(tl.Manager.prototype.install).toBeCalledWith([]); + expect(core.saveState).toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', false); + }); - notIf(inputs.cache)(expect(cache.restoreCache)).toBeCalled(); - notIf(!inputs.cache)(expect(tl.install)).toBeCalled(); - notIf(inputs.cache)(expect(tl.Manager.prototype.pathAdd)).toBeCalled(); - expect(tl.Manager.prototype.install).toBeCalledWith(data.packages); - notIf(inputs.cache)(expect(core.saveState)).toBeCalledWith( - 'key', - expect.anything(), - ); - expect(core.saveState).toBeCalledWith('post', true); - expect(core.setOutput).toBeCalledWith('cache-hit', inputs.cache); - }); + it('sets up TeX Live with custom settings', async () => { + context.inputs.cache = false; + context.inputs.packages = ' foo bar\n baz'; + context.inputs.prefix = '/usr/local/texlive'; + context.inputs.version = '2008'; + await setup.run(); + expect(cache.restoreCache).not.toBeCalled(); + expect(tl.install).toBeCalledWith('2008', '/usr/local/texlive', 'linux'); + expect(tl.Manager.prototype.install).toBeCalledWith(['bar', 'baz', 'foo']); + expect(core.saveState).not.toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', false); + }); - test('with full cache', async () => { - (cache.restoreCache as jest.Mock).mockImplementation( - async (paths, primaryKey) => primaryKey, - ); + it('sets up TeX Live with a system cache', async () => { + (cache.restoreCache as jest.Mock).mockImplementationOnce( + async (paths, primaryKey, restoreKeys) => restoreKeys?.[0] ?? '', + ); + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).not.toBeCalled(); + expect(tl.Manager.prototype.install).toBeCalled(); + expect(core.saveState).toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', true); + }); - await setup.run(); + it('sets up TeX Live with a full cache', async () => { + (cache.restoreCache as jest.Mock).mockImplementationOnce( + async (paths, primaryKey) => primaryKey, + ); + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).not.toBeCalled(); + expect(tl.Manager.prototype.install).not.toBeCalled(); + expect(core.saveState).not.toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', true); + }); - notIf(inputs.cache)(expect(cache.restoreCache)).toBeCalled(); - notIf(!inputs.cache)(expect(tl.install)).toBeCalled(); - notIf(inputs.cache)(expect(tl.Manager.prototype.pathAdd)).toBeCalled(); - notIf(!inputs.cache)(expect(tl.Manager.prototype.install)).toBeCalled(); - expect(core.saveState).not.toBeCalledWith('key', expect.anything()); - expect(core.saveState).toBeCalledWith('post', true); - expect(core.setOutput).toBeCalledWith('cache-hit', inputs.cache); + it('continues setup even if `cache.restoreCache` throws an exception', async () => { + (cache.restoreCache as jest.Mock).mockImplementationOnce(async () => { + throw new Error('oops'); }); + await setup.run(); + expect(cache.restoreCache).toBeCalled(); + expect(tl.install).toBeCalled(); + expect(tl.Manager.prototype.install).toBeCalled(); + expect(core.saveState).toBeCalledWith('key', expect.anything()); + expect(core.saveState).toBeCalledWith('post', true); + expect(core.setOutput).toBeCalledWith('cache-hit', false); }); - test('caching is disabled neither `ACTIONS_CACHE_URL` nor `ACTIONS_RUNTIME_URL` is set', async () => { + it('disables caching if proper environment variables are not set', async () => { process.env['ACTIONS_CACHE_URL'] = undefined; process.env['ACTIONS_RUNTIME_URL'] = undefined; - await setup.run(); expect(cache.restoreCache).not.toBeCalled(); expect(core.saveState).not.toBeCalledWith('key', expect.anything()); }); - test.each(['2018', '2022', 'version'])( - 'with invalid version input', + it.each(['1995', '2022', 'version'])( + 'fails with the invalid version input', async (version) => { context.inputs.version = version; await expect(setup.run()).rejects.toThrow( @@ -248,13 +208,13 @@ describe('setup()', () => { ); }); -describe('saveCache()', () => { +describe('saveCache', () => { beforeEach(() => { context.state.post = 'true'; }); - test('with `key', async () => { - context.state.key = 'setup-texlive-primary-key'; + it('saves `TEXDIR` if `key` is set', async () => { + context.state.key = random(); await setup.run(); expect(cache.saveCache).toBeCalledWith( expect.arrayContaining([]), @@ -262,7 +222,7 @@ describe('saveCache()', () => { ); }); - test('without `key`', async () => { + it('does nothing if `key` is not set', async () => { await setup.run(); expect(cache.saveCache).not.toBeCalled(); }); diff --git a/__tests__/texlive.test.ts b/__tests__/texlive.test.ts index 42ea59d..0d77369 100644 --- a/__tests__/texlive.test.ts +++ b/__tests__/texlive.test.ts @@ -9,8 +9,8 @@ import * as tool from '@actions/tool-cache'; import * as tl from '#/texlive'; -jest.spyOn(fs, 'mkdtemp'); -jest.spyOn(fs, 'writeFile'); +const random = (): string => (Math.random() + 1).toString(32).substring(7); + jest.mock('os', () => ({ tmpdir: jest.fn(), })); @@ -22,39 +22,30 @@ jest.mock('path', () => { win32: actual.win32, }; }); -jest.spyOn(core, 'addPath'); -jest.spyOn(exec, 'exec'); +(os.tmpdir as jest.Mock).mockReturnValue(random()); +jest.spyOn(fs, 'mkdtemp').mockResolvedValue(random()); +jest.spyOn(fs, 'readFile').mockResolvedValue(''); +jest.spyOn(fs, 'writeFile').mockImplementation(); +jest.spyOn(core, 'addPath').mockImplementation(); +jest.spyOn(exec, 'exec').mockImplementation(); jest.spyOn(glob, 'create'); -jest.spyOn(tool, 'cacheDir'); -jest.spyOn(tool, 'downloadTool'); -jest.spyOn(tool, 'find'); -jest.spyOn(tool, 'extractTar'); -jest.spyOn(tool, 'extractZip'); - -const random = (): string => (Math.random() + 1).toString(32).substring(7); +jest.spyOn(tool, 'cacheDir').mockResolvedValue(''); +jest.spyOn(tool, 'downloadTool').mockResolvedValue(random()); +jest.spyOn(tool, 'extractTar').mockResolvedValue(random()); +jest.spyOn(tool, 'extractZip').mockResolvedValue(random()); +jest.spyOn(tool, 'find').mockReturnValue(''); beforeEach(() => { console.log('::stop-commands::stoptoken'); process.env['GITHUB_PATH'] = ''; - (fs.mkdtemp as jest.Mock).mockResolvedValue(random()); - (fs.writeFile as jest.Mock).mockImplementation(); - (os.tmpdir as jest.Mock).mockReturnValue(random()); (path.join as jest.Mock).mockImplementation(path.posix.join); - (core.addPath as jest.Mock).mockImplementation(); - (exec.exec as jest.Mock).mockImplementation(); (glob.create as jest.Mock).mockResolvedValue({ glob: async (): Promise> => [], } as glob.Globber); - (tool.cacheDir as jest.Mock).mockResolvedValue(''); - (tool.downloadTool as jest.Mock).mockResolvedValue(random()); - (tool.extractTar as jest.Mock).mockResolvedValue(random()); - (tool.extractZip as jest.Mock).mockResolvedValue(random()); - (tool.find as jest.Mock).mockReturnValue(''); }); afterEach(() => { - jest.resetAllMocks(); jest.clearAllMocks(); }); @@ -63,75 +54,40 @@ afterAll(async () => { }, 100000); test.each([ - ['2018', false], - ['2019', true], + ['1995', false], + ['1996', true], + ['2008', true], + ['2015', true], ['2021', true], + ['2022', false], ['latest', false], ])('isVersion(%o)', (version, result) => { expect(tl.isVersion(version)).toBe(result); }); describe('Manager', () => { - test.each<[tl.Version, string, NodeJS.Platform, tl.Texmf]>([ - [ - '2019', - '/usr/local/texlive', - 'linux', - { - texdir: '/usr/local/texlive/2019', - local: '/usr/local/texlive/texmf-local', - sysconfig: '/usr/local/texlive/2019/texmf-config', - sysvar: '/usr/local/texlive/2019/texmf-var', - }, - ], - [ - '2019', - 'C:\\texlive', - 'win32', - { - texdir: 'C:\\texlive\\2019', - local: 'C:\\texlive\\texmf-local', - sysconfig: 'C:\\texlive\\2019\\texmf-config', - sysvar: 'C:\\texlive\\2019\\texmf-var', - }, - ], - ])('conf() on $platform', (version, prefix, platform, texmf) => { - (path.join as jest.Mock).mockImplementation( - platform === 'win32' ? path.win32.join : path.posix.join, - ); - const tlmgr = new tl.Manager(version, prefix); - expect(tlmgr.conf()).toStrictEqual(texmf); - }); + const tlmgr = new tl.Manager('2019', '/usr/local/texlive'); - describe('install(packages)', () => { - const tlmgr = new tl.Manager('2019', '/usr/local/texlive'); - - test('with []', async () => { + describe('install', () => { + it('does not invoke `tlmgr install` if the argument is empty', async () => { await tlmgr.install([]); expect(exec.exec).not.toBeCalled(); }); - test("with ['foo', 'bar', 'baz']", async () => { - await tlmgr.install(['foo', 'bar', 'baz']); - expect(exec.exec).toBeCalledWith('tlmgr', [ - 'install', - 'foo', - 'bar', - 'baz', - ]); + it('installs packages by invoking `tlmgr install`', async () => { + const packages = ['foo', 'bar', 'baz']; + await tlmgr.install(packages); + expect(exec.exec).toBeCalledWith('tlmgr', ['install', ...packages]); }); }); - describe('pathAdd()', () => { - const tlmgr = new tl.Manager('2019', '/usr/local/texlive'); - - it('succeeds', async () => { + describe('pathAdd', () => { + it('adds the bin directory to the PATH', async () => { (glob.create as jest.Mock).mockImplementation(async (pattern) => { return { glob: async () => [pattern.replace('*', 'x86_64-linux')], } as glob.Globber; }); - await tlmgr.pathAdd(); expect(core.addPath).toBeCalledWith( '/usr/local/texlive/2019/bin/x86_64-linux', @@ -142,13 +98,12 @@ describe('Manager', () => { [[]], [['x86_64-linux', 'universal-darwin']], [['x86_64-linux', 'universal-darwin', 'Windows']], - ])('fails as directory cannot be located', async (matched) => { + ])('fails as the bin directory cannot be located', async (matched) => { (glob.create as jest.Mock).mockImplementation(async (pattern) => { return { glob: async () => matched.map((x) => pattern.replace('*', x)), } as glob.Globber; }); - await expect(tlmgr.pathAdd()).rejects.toThrow( 'Unable to locate the bin directory', ); @@ -156,195 +111,264 @@ describe('Manager', () => { }); }); -describe.each< - [ - tl.Version, - string, - NodeJS.Platform, - { profile: string; texdir: string; tool: string }, - ] ->([ - [ - '2019', - '/usr/local/texlive', - 'linux', - { - profile: `TEXDIR /usr/local/texlive/2019 -TEXMFLOCAL /usr/local/texlive/texmf-local -TEXMFSYSCONFIG /usr/local/texlive/2019/texmf-config -TEXMFSYSVAR /usr/local/texlive/2019/texmf-var -selected_scheme scheme-infraonly -instopt_adjustrepo 0 -tlpdbopt_autobackup 0 -tlpdbopt_desktop_integration 0 -tlpdbopt_file_assocs 0 -tlpdbopt_install_docfiles 0 -tlpdbopt_install_srcfiles 0 -tlpdbopt_w32_multi_user 0`, - texdir: '/usr/local/texlive/2019', - tool: 'https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2019/tlnet-final/install-tl-unx.tar.gz', - }, - ], - [ - '2021', - '/usr/local/texlive', - 'linux', - { - profile: `TEXDIR /usr/local/texlive/2021 -TEXMFLOCAL /usr/local/texlive/texmf-local -TEXMFSYSCONFIG /usr/local/texlive/2021/texmf-config -TEXMFSYSVAR /usr/local/texlive/2021/texmf-var -selected_scheme scheme-infraonly -instopt_adjustrepo 1 -tlpdbopt_autobackup 0 -tlpdbopt_desktop_integration 0 -tlpdbopt_file_assocs 0 -tlpdbopt_install_docfiles 0 -tlpdbopt_install_srcfiles 0 -tlpdbopt_w32_multi_user 0`, - texdir: '/usr/local/texlive/2021', - tool: 'https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz', - }, - ], - [ - '2019', - 'C:\\texlive', - 'win32', - { - profile: `TEXDIR C:\\texlive\\2019 -TEXMFLOCAL C:\\texlive\\texmf-local -TEXMFSYSCONFIG C:\\texlive\\2019\\texmf-config -TEXMFSYSVAR C:\\texlive\\2019\\texmf-var -selected_scheme scheme-infraonly -instopt_adjustrepo 0 -tlpdbopt_autobackup 0 -tlpdbopt_desktop_integration 0 -tlpdbopt_file_assocs 0 -tlpdbopt_install_docfiles 0 -tlpdbopt_install_srcfiles 0 -tlpdbopt_w32_multi_user 0`, - texdir: 'C:\\texlive\\2019', - tool: 'https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2019/tlnet-final/install-tl.zip', - }, - ], - [ - '2021', - 'C:\\texlive', - 'win32', - { - profile: `TEXDIR C:\\texlive\\2021 -TEXMFLOCAL C:\\texlive\\texmf-local -TEXMFSYSCONFIG C:\\texlive\\2021\\texmf-config -TEXMFSYSVAR C:\\texlive\\2021\\texmf-var -selected_scheme scheme-infraonly -instopt_adjustrepo 1 -tlpdbopt_autobackup 0 -tlpdbopt_desktop_integration 0 -tlpdbopt_file_assocs 0 -tlpdbopt_install_docfiles 0 -tlpdbopt_install_srcfiles 0 -tlpdbopt_w32_multi_user 0`, - texdir: 'C:\\texlive\\2021', - tool: 'https://mirror.ctan.org/systems/texlive/tlnet/install-tl.zip', - }, - ], -])('install(%o, %o, %o)', (version, prefix, platform, data) => { +describe('install', () => { beforeEach(() => { - (path.join as jest.Mock).mockImplementation( - platform === 'win32' ? path.win32.join : path.posix.join, - ); - }); - - test('by downloading', async () => { (glob.create as jest.Mock).mockImplementation(async (pattern) => { return { - glob: async () => [pattern.replace('*', 'matched')], + glob: async () => [pattern.replace('*', random())], } as glob.Globber; }); + }); - await tl.install(version, prefix, platform); - - expect(tool.find).toBeCalledWith(path.posix.basename(data.tool), version); - expect(tool.downloadTool).toBeCalledWith(data.tool); + it('installs TeX Live 2008 on Linux', async () => { + await tl.install('2008', '/usr/local/texlive', 'linux'); + expect(tool.find).toBeCalledWith('install-tl-unx.tar.gz', '2008'); + expect(tool.downloadTool).toBeCalledWith( + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2008/tlnet/install-tl-unx.tar.gz', + ); expect(fs.writeFile).toBeCalledWith( - expect.stringMatching(/texlive.profile$/), - data.profile, + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL /usr/local/texlive/texmf-local', + 'TEXMFSYSCONFIG /usr/local/texlive/2008/texmf-config', + 'TEXMFSYSVAR /usr/local/texlive/2008/texmf-var', + 'selected_scheme scheme-minimal', + 'option_adjustrepo 0', + ].join('\n'), + ), ); expect(exec.exec).toBeCalledWith( - expect.stringMatching( - platform === 'win32' ? /install-tl-windows$/ : /install-tl$/, - ), + expect.stringMatching(/install-tl$/), [ '-no-gui', '-profile', - expect.stringMatching(/texlive.profile$/), - ...(version === tl.LATEST_VERSION - ? [] - : [ - '-repository', - expect.stringMatching(path.posix.dirname(data.tool)), - ]), + expect.stringMatching(/texlive\.profile$/), + '-location', + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2008/tlnet/', ], - expect.objectContaining({ - env: expect.objectContaining({ - TEXLIVE_INSTALL_ENV_NOCHECK: expect.anything(), - }), - }), + expect.anything(), ); expect(core.addPath).toBeCalledWith( - expect.stringContaining(path.join(data.texdir, 'bin')), + expect.stringContaining('/usr/local/texlive/2008/bin/'), ); expect(tool.cacheDir).toBeCalled(); }); - test('with cache', async () => { - (glob.create as jest.Mock).mockImplementation(async (pattern) => { - return { - glob: async () => [pattern.replace('*', 'matched')], - } as glob.Globber; - }); - (tool.find as jest.Mock).mockReturnValue(random()); + it('installs TeX Live 2008 on Windows', async () => { + (path.join as jest.Mock).mockImplementation(path.win32.join); + await tl.install('2008', 'C:\\texlive', 'win32'); + expect(tool.find).toBeCalledWith('install-tl.zip', '2008'); + expect(tool.downloadTool).toBeCalledWith( + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2008/tlnet/install-tl.zip', + ); + expect(fs.writeFile).toBeCalledWith( + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL C:\\texlive\\texmf-local', + 'TEXMFSYSCONFIG C:\\texlive\\2008\\texmf-config', + 'TEXMFSYSVAR C:\\texlive\\2008\\texmf-var', + 'selected_scheme scheme-minimal', + 'option_adjustrepo 0', + ].join('\n'), + ), + ); + expect(exec.exec).toBeCalledWith( + expect.stringMatching(/install-tl.bat/), + [ + '-no-gui', + '-profile', + expect.stringMatching(/texlive\.profile$/), + '-location', + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2008/tlnet/', + ], + expect.anything(), + ); + expect(core.addPath).toBeCalledWith( + expect.stringContaining('C:\\texlive\\2008\\bin\\'), + ); + expect(tool.cacheDir).toBeCalled(); + }); - await tl.install(version, prefix, platform); + it('installs TeX Live 2013 on Windows', async () => { + (path.join as jest.Mock).mockImplementation(path.win32.join); + await tl.install('2013', 'C:\\texlive', 'win32'); + expect(tool.find).toBeCalledWith('install-tl.zip', '2013'); + expect(tool.downloadTool).toBeCalledWith( + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2013/tlnet-final/install-tl.zip', + ); + expect(fs.writeFile).toBeCalledWith( + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL C:\\texlive\\texmf-local', + 'TEXMFSYSCONFIG C:\\texlive\\2013\\texmf-config', + 'TEXMFSYSVAR C:\\texlive\\2013\\texmf-var', + 'selected_scheme scheme-minimal', + 'option_adjustrepo 0', + ].join('\n'), + ), + ); + expect(exec.exec).toBeCalledWith( + expect.stringMatching(/install-tl-windows.bat/), + [ + '-no-gui', + '-profile', + expect.stringMatching(/texlive\.profile$/), + '-repository', + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2013/tlnet-final/', + ], + expect.anything(), + ); + expect(core.addPath).toBeCalledWith( + expect.stringContaining('C:\\texlive\\2013\\bin\\'), + ); + expect(tool.cacheDir).toBeCalled(); + }); - expect(tool.find).toBeCalledWith(path.posix.basename(data.tool), version); - expect(tool.downloadTool).not.toBeCalled(); + it('installs TeX Live 2013 on macOS', async () => { + await tl.install('2013', '/usr/local/texlive', 'darwin'); + expect(tool.find).toBeCalledWith('install-tl-unx.tar.gz', '2013'); + expect(tool.downloadTool).toBeCalledWith( + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2013/tlnet-final/install-tl-unx.tar.gz', + ); expect(fs.writeFile).toBeCalledWith( - expect.stringMatching(/texlive.profile$/), - data.profile, + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL /usr/local/texlive/texmf-local', + 'TEXMFSYSCONFIG /usr/local/texlive/2013/texmf-config', + 'TEXMFSYSVAR /usr/local/texlive/2013/texmf-var', + 'selected_scheme scheme-minimal', + 'option_adjustrepo 0', + ].join('\n'), + ), ); expect(exec.exec).toBeCalledWith( - expect.stringMatching( - platform === 'win32' ? /install-tl-windows$/ : /install-tl$/, + expect.stringMatching(/install-tl$/), + [ + '-no-gui', + '-profile', + expect.stringMatching(/texlive\.profile$/), + '-repository', + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2013/tlnet-final/', + ], + expect.anything(), + ); + expect(core.addPath).toBeCalledWith( + expect.stringContaining('/usr/local/texlive/2013/bin/'), + ); + expect(tool.cacheDir).toBeCalled(); + }); + + it('installs TeX Live 2016 on macOS', async () => { + await tl.install('2016', '/usr/local/texlive', 'darwin'); + expect(tool.find).toBeCalledWith('install-tl-unx.tar.gz', '2016'); + expect(tool.downloadTool).toBeCalledWith( + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2016/tlnet-final/install-tl-unx.tar.gz', + ); + expect(fs.writeFile).toBeCalledWith( + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL /usr/local/texlive/texmf-local', + 'TEXMFSYSCONFIG /usr/local/texlive/2016/texmf-config', + 'TEXMFSYSVAR /usr/local/texlive/2016/texmf-var', + 'selected_scheme scheme-infraonly', + 'option_adjustrepo 0', + ].join('\n'), ), + ); + expect(exec.exec).toBeCalledWith( + expect.stringMatching(/install-tl$/), [ '-no-gui', '-profile', - expect.stringMatching(/texlive.profile$/), - ...(version === tl.LATEST_VERSION - ? [] - : [ - '-repository', - expect.stringMatching(path.posix.dirname(data.tool)), - ]), + expect.stringMatching(/texlive\.profile$/), + '-repository', + 'http://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2016/tlnet-final/', ], - expect.objectContaining({ - env: expect.objectContaining({ - TEXLIVE_INSTALL_ENV_NOCHECK: expect.anything(), - }), - }), + expect.anything(), + ); + expect(core.addPath).toBeCalledWith( + expect.stringContaining('/usr/local/texlive/2016/bin/'), + ); + expect(tool.cacheDir).toBeCalled(); + }); + + it('installs the latest version of TeX Live on Linux', async () => { + await tl.install('2021', '/usr/local/texlive', 'linux'); + expect(tool.find).toBeCalledWith('install-tl-unx.tar.gz', '2021'); + expect(tool.downloadTool).toBeCalledWith( + 'https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz', + ); + expect(fs.writeFile).toBeCalledWith( + expect.stringMatching(/texlive\.profile$/), + expect.stringContaining( + [ + 'TEXMFLOCAL /usr/local/texlive/texmf-local', + 'TEXMFSYSCONFIG /usr/local/texlive/2021/texmf-config', + 'TEXMFSYSVAR /usr/local/texlive/2021/texmf-var', + 'selected_scheme scheme-infraonly', + 'option_adjustrepo 1', + ].join('\n'), + ), + ); + expect(exec.exec).toBeCalledWith( + expect.stringMatching(/install-tl$/), + ['-no-gui', '-profile', expect.stringMatching(/texlive\.profile$/)], + expect.anything(), ); expect(core.addPath).toBeCalledWith( - expect.stringContaining(path.join(data.texdir, 'bin')), + expect.stringContaining('/usr/local/texlive/2021/bin/'), ); + expect(tool.cacheDir).toBeCalled(); + }); + + it('installs TeX Live with a installer cache', async () => { + (tool.find as jest.Mock).mockReturnValueOnce(random()); + await tl.install('2021', '/usr/local/texlive', 'linux'); + expect(tool.find).toBeCalledWith('install-tl-unx.tar.gz', '2021'); + expect(tool.downloadTool).not.toBeCalled(); expect(tool.cacheDir).not.toBeCalled(); }); - if (platform === 'win32') { - it('fails as installer cannot be located', async () => { - await expect(tl.install(version, prefix, platform)).rejects.toThrow( - 'Unable to locate the installer path', - ); + it('continues the installation even if `tool-cache` throws an exception', async () => { + (tool.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('oops'); }); - } + (tool.cacheDir as jest.Mock).mockImplementationOnce(() => { + throw new Error('oops'); + }); + await expect( + tl.install('2021', '/usr/local/texlive', 'linux'), + ).resolves.not.toThrow(); + }); + + it('fails as the installer cannot be located', async () => { + (glob.create as jest.Mock).mockReturnValue({ + glob: async () => [] as Array, + } as glob.Globber); + await expect(tl.install('2021', 'C:\\texlive', 'win32')).rejects.toThrow( + 'Unable to locate the installer path', + ); + }); + + it.each<[tl.Version, NodeJS.Platform]>([ + ['2007', 'linux'], + ['2007', 'win32'], + ['2012', 'darwin'], + ])( + 'does not support the installation of TeX Live %s on %s', + async (version, platform) => { + await expect( + tl.install(version, '/usr/local/texlive', platform), + ).rejects.toThrow( + /^Installation of TeX Live \d{4} on (?:\w+) is not supported$/, + ); + }, + ); + + test.todo('tests for `patch`'); }); diff --git a/action.yml b/action.yml index da1990a..d4f232f 100644 --- a/action.yml +++ b/action.yml @@ -22,7 +22,8 @@ inputs: version: description: >- Version of TeX Live to install. - Supported values are `2019`, `2020`, `2021`, and `latest`. + Supported versions are `2008` to `2021` for Linux and Windows, and + `2013` to `2021` for macOS. If set to `latest`, the latest version will be installed. default: latest required: false diff --git a/dist/index.js b/dist/index.js index 633e793..00504e2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -59838,19 +59838,14 @@ function getInputs() { process.env['TEXLIVE_INSTALL_PREFIX'], path.join(os.platform() === 'win32' ? 'C:\\TEMP' : '/tmp', 'setup-texlive'), ].find(Boolean); - const getVersion = (version) => { - if (version === 'latest') { - return tl.LATEST_VERSION; - } - else if (tl.isVersion(version)) { - return version; - } - else { - throw new Error("`version` must be specified by year or 'latest'"); - } - }; - const version = getVersion(core.getInput('version')); - return { cache, packages, prefix, version }; + let version = core.getInput('version'); + if (version === 'latest') { + version = tl.LATEST_VERSION; + } + else if (!tl.isVersion(version)) { + throw new Error("`version` must be specified by year or 'latest'"); + } + return { cache, packages, prefix, version: version }; } function getCacheKeys(version, packages) { const digest = (s) => { @@ -59943,39 +59938,46 @@ var __importStar = (this && this.__importStar) || function (mod) { __setModuleDefault(result, mod); return result; }; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _InstallTL_instances, _InstallTL_download, _InstallTL_profile; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Manager = exports.install = exports.LATEST_VERSION = exports.isVersion = void 0; const fs_1 = __nccwpck_require__(7147); const os = __importStar(__nccwpck_require__(2037)); const path = __importStar(__nccwpck_require__(1017)); +const url_1 = __nccwpck_require__(7310); const core = __importStar(__nccwpck_require__(2186)); const exec = __importStar(__nccwpck_require__(1514)); const glob = __importStar(__nccwpck_require__(8090)); const tool = __importStar(__nccwpck_require__(7784)); // prettier-ignore const VERSIONS = [ - '2019', - '2020', - '2021', + '1996', '1997', '1998', '1999', + '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', + '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018', '2019', + '2020', '2021', ]; function isVersion(version) { return VERSIONS.includes(version); } exports.isVersion = isVersion; -exports.LATEST_VERSION = '2021'; -async function install(version, prefix, platform) { - const installer = path.join(await download(version, platform), `install-tl${platform === 'win32' ? '-windows' : ''}`); - const profile = await createProfile(version, prefix); - const env = { ...process.env, TEXLIVE_INSTALL_ENV_NOCHECK: '1' }; - const options = ['-no-gui', '-profile', profile]; - if (version !== exports.LATEST_VERSION) { - options.push('-repository', repository(version).toString()); - } - await core.group('Installing TeX Live', async () => { - await exec.exec(installer, options, { env }); - const tlmgr = new Manager(version, prefix); - await tlmgr.pathAdd(); - }); +exports.LATEST_VERSION = VERSIONS[VERSIONS.length - 1]; +async function install(version, prefix, platform = os.platform()) { + /** + * - There is no `install-tl` for versions prior to 2005, and + * versions 2005--2007 do not seem to be archived. + * + * - Versions 2008--2012 can be installed on `macos-latest`, but + * do not work properly because the `kpsewhich aborts with "Bad CPU type." + */ + if (Number(version) < (platform === 'darwin' ? 2013 : 2008)) { + throw new Error(`Installation of TeX Live ${version} on ${platform} is not supported`); + } + return new InstallTL(version, prefix, platform).run(); } exports.install = install; class Manager { @@ -59991,45 +59993,75 @@ class Manager { return { texdir, local, sysconfig, sysvar }; } async install(packages) { - if (packages.length === 0) { - return; + if (packages.length !== 0) { + await exec.exec('tlmgr', ['install', ...packages]); } - await exec.exec('tlmgr', ['install', ...packages]); } /** * @todo `install-tl -print-platform` and `tlmgr print-platform` may be useful */ async pathAdd() { - const bin = await determine(path.join(this.prefix, this.version, 'bin', '*')); - if (bin === undefined) { + const matched = await expand(path.join(this.prefix, this.version, 'bin', '*')); + if (matched.length !== 1) { + core.debug(`Matched: ${matched}`); throw new Error('Unable to locate the bin directory'); } - core.addPath(bin); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + core.addPath(matched[0]); } } exports.Manager = Manager; -/** - * Returns the only file path that matches the given glob pattern. - * If it is not uniquely determined, it returns `undefined`. - * - * @param pattern - A glob pattern - * @returns - The path of the matched file or directory - */ -async function determine(pattern) { - const globber = await glob.create(pattern, { implicitDescendants: false }); - const matched = await globber.glob(); - return matched.length === 1 ? matched[0] : undefined; -} function repository(version) { - return new URL(version === exports.LATEST_VERSION - ? 'https://mirror.ctan.org/systems/texlive/tlnet/' - : `https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/${version}/tlnet-final/`); + const base = version === exports.LATEST_VERSION + ? 'https://mirror.ctan.org/systems/texlive/' + : `https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/${version}/`; + const tlnet = `tlnet${Number(version) < 2010 || version === exports.LATEST_VERSION ? '' : '-final'}/`; + const url = new url_1.URL(tlnet, base); + /** + * `install-tl` of versions prior to 2017 does not support HTTPS, and + * that of version 2017 supports HTTPS but does not work properly. + */ + if (Number(version) < 2018) { + url.protocol = 'http'; + } + return url; } -async function download(version, platform) { - const filename = `install-tl${platform === 'win32' ? '.zip' : '-unx.tar.gz'}`; - return core.group(`Arquiring ${filename}`, async () => { +class InstallTL { + constructor(version, prefix, platform) { + this.version = version; + this.prefix = prefix; + this.platform = platform; + _InstallTL_instances.add(this); + } + async run() { + const installtl = path.join(await __classPrivateFieldGet(this, _InstallTL_instances, "m", _InstallTL_download).call(this), InstallTL.executable(this.version, this.platform)); + const env = { ...process.env, TEXLIVE_INSTALL_ENV_NOCHECK: '1' }; + const options = ['-no-gui', '-profile', await __classPrivateFieldGet(this, _InstallTL_instances, "m", _InstallTL_profile).call(this)]; + if (this.version !== exports.LATEST_VERSION) { + options.push( + /** + * Only version 2008 uses `-location` instead of `-repository`. + */ + this.version === '2008' ? '-location' : '-repository', repository(this.version).href); + } + await core.group('Installing TeX Live', async () => { + await exec.exec(installtl, options, { env }); + const tlmgr = new Manager(this.version, this.prefix); + core.info('Applying patches'); + await patch(this.version, this.platform, tlmgr.conf().texdir); + await tlmgr.pathAdd(); + }); + } + static executable(version, platform) { + const ext = `${Number(version) > 2012 ? '-windows' : ''}.bat`; + return `install-tl${platform === 'win32' ? ext : ''}`; + } +} +_InstallTL_instances = new WeakSet(), _InstallTL_download = async function _InstallTL_download() { + const target = `install-tl${this.platform === 'win32' ? '.zip' : '-unx.tar.gz'}`; + return core.group(`Acquiring ${target}`, async () => { try { - const cache = tool.find(filename, version); + const cache = tool.find(target, this.version); if (cache !== '') { core.info('Found in cache'); return cache; @@ -60041,25 +60073,28 @@ async function download(version, platform) { core.debug(error.stack); } } - const url = `${repository(version)}${filename}`; + const url = new url_1.URL(target, repository(this.version)).href; core.info(`Downloading ${url}`); const archive = await tool.downloadTool(url); core.info('Extracting'); let dest; - if (platform === 'win32') { - const sub = await determine(path.join(await tool.extractZip(archive), 'install-tl-*')); - if (sub === undefined) { + if (this.platform === 'win32') { + const matched = await expand(path.join(await tool.extractZip(archive), 'install-tl*')); + if (matched.length !== 1) { + core.debug(`Matched: ${matched}`); throw new Error('Unable to locate the installer path'); } - dest = sub; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dest = matched[0]; } else { - const options = ['xz', '--strip=1']; - dest = await tool.extractTar(archive, undefined, options); + dest = await tool.extractTar(archive, undefined, ['xz', '--strip=1']); } + core.info('Applying patches'); + await patch(this.version, this.platform, dest); try { - core.info(`Adding to the cache`); - await tool.cacheDir(dest, filename, version); + core.info('Adding to the cache'); + await tool.cacheDir(dest, target, this.version); } catch (error) { core.info(`Failed to add to cache: ${error}`); @@ -60069,37 +60104,94 @@ async function download(version, platform) { } return dest; }); -} -async function createProfile(version, prefix) { +}, _InstallTL_profile = async function _InstallTL_profile() { var _a; - const tlmgr = new Manager(version, prefix); - const texmf = tlmgr.conf(); - const adjustrepo = version === exports.LATEST_VERSION ? 1 : 0; - const profile = ` - TEXDIR ${texmf.texdir} - TEXMFLOCAL ${texmf.local} - TEXMFSYSCONFIG ${texmf.sysconfig} - TEXMFSYSVAR ${texmf.sysvar} - selected_scheme scheme-infraonly - instopt_adjustrepo ${adjustrepo} - tlpdbopt_autobackup 0 - tlpdbopt_desktop_integration 0 - tlpdbopt_file_assocs 0 - tlpdbopt_install_docfiles 0 - tlpdbopt_install_srcfiles 0 - tlpdbopt_w32_multi_user 0 - ` - .trim() - .split(/\n\s*/) - .join('\n'); - core.startGroup('Profile'); - core.info(profile); - core.endGroup(); - const tmp = (_a = process.env['RUNNER_TEMP']) !== null && _a !== void 0 ? _a : os.tmpdir(); - const dest = path.join(await fs_1.promises.mkdtemp(path.join(tmp, 'setup-texlive-')), 'texlive.profile'); + const texmf = new Manager(this.version, this.prefix).conf(); + const adjustrepo = this.version === exports.LATEST_VERSION ? 1 : 0; + /** + * `scheme-infraonly` was first introduced in TeX Live 2016. + */ + const scheme = Number(this.version) < 2016 ? 'minimal' : 'infraonly'; + const profile = [ + `TEXDIR ${texmf.texdir}`, + `TEXMFLOCAL ${texmf.local}`, + `TEXMFSYSCONFIG ${texmf.sysconfig}`, + `TEXMFSYSVAR ${texmf.sysvar}`, + `selected_scheme scheme-${scheme}`, + `option_adjustrepo ${adjustrepo}`, + 'option_autobackup 0', + 'option_desktop_integration 0', + 'option_doc 0', + 'option_file_assocs 0', + 'option_menu_integration 0', + 'option_src 0', + 'option_w32_multi_user 0', + ].join('\n'); + core.group('Profile', async () => core.info(profile)); + const dest = path.join(await fs_1.promises.mkdtemp(path.join((_a = process.env['RUNNER_TEMP']) !== null && _a !== void 0 ? _a : os.tmpdir(), 'setup-texlive-')), 'texlive.profile'); await fs_1.promises.writeFile(dest, profile); core.debug(`${dest} created`); return dest; +}; +async function patch(version, platform, texdir) { + var _a; + const update = async (filename, map) => { + return fs_1.promises.writeFile(filename, map(await fs_1.promises.readFile(filename, 'utf8'))); + }; + /** + * Prevent `install-tl(-windows).bat` from being stopped by `pause`. + */ + if (platform === 'win32') { + try { + await update(path.join(texdir, InstallTL.executable(version, platform)), (content) => content.replace(/\bpause(?: Done)?\b/gm, '')); + } + catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (((_a = error) === null || _a === void 0 ? void 0 : _a.code) !== 'ENOENT') { + throw error; + } + } + } + /** + * Fix a syntax error in `tlpkg/TeXLive/TLWinGoo.pm`. + */ + if (['2009', '2010'].includes(version)) { + await update(path.join(texdir, 'tlpkg', 'TeXLive', 'TLWinGoo.pm'), (content) => { + return content.replace(/foreach \$p qw\((.*)\)/, 'foreach $$p (qw($1))'); + }); + } + /** + * Define Code Page 65001 as an alias for UTF-8 on Windows. + * @see {@link https://github.com/dankogai/p5-encode/issues/37} + */ + if (platform === 'win32' && version === '2015') { + await update(path.join(texdir, 'tlpkg', 'tlperl', 'lib', 'Encode', 'Alias.pm'), (content) => { + return content.replace('# utf8 is blessed :)', `define_alias(qr/cp65001/i => '"utf-8-strict"');`); + }); + } + /** + * Make it possible to use `\` as a directory separator on Windows. + */ + if (platform === 'win32' && Number(version) < 2019) { + await update(path.join(texdir, 'tlpkg', 'TeXLive', 'TLUtils.pm'), (content) => { + return content.replace(String.raw `split (/\//, $tree)`, String.raw `split (/[\/\\]/, $tree)`); + }); + } + /** + * Add support for macOS 11.x. + */ + if (platform === 'darwin' && ['2017', '2018', '2019'].includes(version)) { + await update(path.join(texdir, 'tlpkg', 'TeXLive', 'TLUtils.pm'), (content) => { + return content + .replace( + // prettier-ignore + 'if ($os_major != 10)', 'if ($$os_major < 10)') + .replace('if ($os_minor >= $mactex_darwin)', 'if ($$os_major >= 11) { $$CPU = "x86_64"; $$OS = "darwin"; } els$&'); + }); + } +} +async function expand(pattern) { + return (await glob.create(pattern, { implicitDescendants: false })).glob(); } diff --git a/src/setup.ts b/src/setup.ts index 0a306c7..71ab542 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -52,19 +52,15 @@ function getInputs(): Inputs { path.join(os.platform() === 'win32' ? 'C:\\TEMP' : '/tmp', 'setup-texlive'), ].find(Boolean) as string; - const getVersion = (version: string): tl.Version => { - if (version === 'latest') { - return tl.LATEST_VERSION; - } else if (tl.isVersion(version)) { - return version; - } else { - throw new Error("`version` must be specified by year or 'latest'"); - } - }; + let version = core.getInput('version'); - const version = getVersion(core.getInput('version')); + if (version === 'latest') { + version = tl.LATEST_VERSION; + } else if (!tl.isVersion(version)) { + throw new Error("`version` must be specified by year or 'latest'"); + } - return { cache, packages, prefix, version }; + return { cache, packages, prefix, version: version as tl.Version }; } function getCacheKeys( diff --git a/src/texlive.ts b/src/texlive.ts index ed103d7..819014b 100644 --- a/src/texlive.ts +++ b/src/texlive.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { URL } from 'url'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; @@ -9,9 +10,10 @@ import * as tool from '@actions/tool-cache'; // prettier-ignore const VERSIONS = [ - '2019', - '2020', - '2021', + '1996', '1997', '1998', '1999', + '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', + '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018', '2019', + '2020', '2021', ] as const; export type Version = typeof VERSIONS[number]; @@ -20,30 +22,26 @@ export function isVersion(version: string): version is Version { return VERSIONS.includes(version as Version); } -export const LATEST_VERSION: Version = '2021'; +export const LATEST_VERSION = VERSIONS[VERSIONS.length - 1] as Version; export async function install( version: Version, prefix: string, - platform: NodeJS.Platform, + platform: NodeJS.Platform = os.platform(), ): Promise { - const installer = path.join( - await download(version, platform), - `install-tl${platform === 'win32' ? '-windows' : ''}`, - ); - const profile = await createProfile(version, prefix); - const env = { ...process.env, TEXLIVE_INSTALL_ENV_NOCHECK: '1' }; - const options = ['-no-gui', '-profile', profile]; - - if (version !== LATEST_VERSION) { - options.push('-repository', repository(version).toString()); + /** + * - There is no `install-tl` for versions prior to 2005, and + * versions 2005--2007 do not seem to be archived. + * + * - Versions 2008--2012 can be installed on `macos-latest`, but + * do not work properly because the `kpsewhich aborts with "Bad CPU type." + */ + if (Number(version) < (platform === 'darwin' ? 2013 : 2008)) { + throw new Error( + `Installation of TeX Live ${version} on ${platform} is not supported`, + ); } - - await core.group('Installing TeX Live', async () => { - await exec.exec(installer, options, { env }); - const tlmgr = new Manager(version, prefix); - await tlmgr.pathAdd(); - }); + return new InstallTL(version, prefix, platform).run(); } export interface Texmf { @@ -54,7 +52,10 @@ export interface Texmf { } export class Manager { - constructor(readonly version: Version, readonly prefix: string) {} + constructor( + private readonly version: Version, + private readonly prefix: string, + ) {} conf(): Texmf { const texdir = path.join(this.prefix, this.version); @@ -65,138 +66,267 @@ export class Manager { } async install(packages: Array): Promise { - if (packages.length === 0) { - return; + if (packages.length !== 0) { + await exec.exec('tlmgr', ['install', ...packages]); } - await exec.exec('tlmgr', ['install', ...packages]); } /** * @todo `install-tl -print-platform` and `tlmgr print-platform` may be useful */ async pathAdd(): Promise { - const bin = await determine( + const matched = await expand( path.join(this.prefix, this.version, 'bin', '*'), ); - if (bin === undefined) { + if (matched.length !== 1) { + core.debug(`Matched: ${matched}`); throw new Error('Unable to locate the bin directory'); } - core.addPath(bin); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + core.addPath(matched[0]!); } } -/** - * Returns the only file path that matches the given glob pattern. - * If it is not uniquely determined, it returns `undefined`. - * - * @param pattern - A glob pattern - * @returns - The path of the matched file or directory - */ -async function determine(pattern: string): Promise { - const globber = await glob.create(pattern, { implicitDescendants: false }); - const matched = await globber.glob(); - return matched.length === 1 ? matched[0] : undefined; -} - function repository(version: Version): URL { - return new URL( + const base = version === LATEST_VERSION - ? 'https://mirror.ctan.org/systems/texlive/tlnet/' - : `https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/${version}/tlnet-final/`, - ); + ? 'https://mirror.ctan.org/systems/texlive/' + : `https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/${version}/`; + const tlnet = `tlnet${ + Number(version) < 2010 || version === LATEST_VERSION ? '' : '-final' + }/`; + const url = new URL(tlnet, base); + /** + * `install-tl` of versions prior to 2017 does not support HTTPS, and + * that of version 2017 supports HTTPS but does not work properly. + */ + if (Number(version) < 2018) { + url.protocol = 'http'; + } + return url; } -async function download( - version: Version, - platform: NodeJS.Platform, -): Promise { - const filename = `install-tl${platform === 'win32' ? '.zip' : '-unx.tar.gz'}`; +class InstallTL { + constructor( + private readonly version: Version, + private readonly prefix: string, + private readonly platform: NodeJS.Platform, + ) {} - return core.group(`Arquiring ${filename}`, async () => { - try { - const cache = tool.find(filename, version); - if (cache !== '') { - core.info('Found in cache'); - return cache; - } - } catch (error) { - core.info(`Failed to restore cache: ${error}`); - if (error instanceof Error && error.stack !== undefined) { - core.debug(error.stack); - } + async run(): Promise { + const installtl = path.join( + await this.#download(), + InstallTL.executable(this.version, this.platform), + ); + const env = { ...process.env, TEXLIVE_INSTALL_ENV_NOCHECK: '1' }; + const options = ['-no-gui', '-profile', await this.#profile()]; + + if (this.version !== LATEST_VERSION) { + options.push( + /** + * Only version 2008 uses `-location` instead of `-repository`. + */ + this.version === '2008' ? '-location' : '-repository', + repository(this.version).href, + ); } - const url = `${repository(version)}${filename}`; - core.info(`Downloading ${url}`); - const archive = await tool.downloadTool(url); + await core.group('Installing TeX Live', async () => { + await exec.exec(installtl, options, { env }); + const tlmgr = new Manager(this.version, this.prefix); + core.info('Applying patches'); + await patch(this.version, this.platform, tlmgr.conf().texdir); + await tlmgr.pathAdd(); + }); + } + + async #download(): Promise { + const target = `install-tl${ + this.platform === 'win32' ? '.zip' : '-unx.tar.gz' + }`; + return core.group(`Acquiring ${target}`, async () => { + try { + const cache = tool.find(target, this.version); + if (cache !== '') { + core.info('Found in cache'); + return cache; + } + } catch (error) { + core.info(`Failed to restore cache: ${error}`); + if (error instanceof Error && error.stack !== undefined) { + core.debug(error.stack); + } + } - core.info('Extracting'); - let dest: string; + const url = new URL(target, repository(this.version)).href; + core.info(`Downloading ${url}`); + const archive = await tool.downloadTool(url); - if (platform === 'win32') { - const sub = await determine( - path.join(await tool.extractZip(archive), 'install-tl-*'), - ); - if (sub === undefined) { - throw new Error('Unable to locate the installer path'); + core.info('Extracting'); + let dest: string; + + if (this.platform === 'win32') { + const matched = await expand( + path.join(await tool.extractZip(archive), 'install-tl*'), + ); + if (matched.length !== 1) { + core.debug(`Matched: ${matched}`); + throw new Error('Unable to locate the installer path'); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dest = matched[0]!; + } else { + dest = await tool.extractTar(archive, undefined, ['xz', '--strip=1']); } - dest = sub; - } else { - const options = ['xz', '--strip=1']; - dest = await tool.extractTar(archive, undefined, options); - } - try { - core.info(`Adding to the cache`); - await tool.cacheDir(dest, filename, version); - } catch (error) { - core.info(`Failed to add to cache: ${error}`); - if (error instanceof Error && error.stack !== undefined) { - core.debug(error.stack); + core.info('Applying patches'); + await patch(this.version, this.platform, dest); + + try { + core.info('Adding to the cache'); + await tool.cacheDir(dest, target, this.version); + } catch (error) { + core.info(`Failed to add to cache: ${error}`); + if (error instanceof Error && error.stack !== undefined) { + core.debug(error.stack); + } } - } + return dest; + }); + } + + async #profile(): Promise { + const texmf = new Manager(this.version, this.prefix).conf(); + const adjustrepo = this.version === LATEST_VERSION ? 1 : 0; + /** + * `scheme-infraonly` was first introduced in TeX Live 2016. + */ + const scheme = Number(this.version) < 2016 ? 'minimal' : 'infraonly'; + const profile = [ + `TEXDIR ${texmf.texdir}`, + `TEXMFLOCAL ${texmf.local}`, + `TEXMFSYSCONFIG ${texmf.sysconfig}`, + `TEXMFSYSVAR ${texmf.sysvar}`, + `selected_scheme scheme-${scheme}`, + `option_adjustrepo ${adjustrepo}`, + 'option_autobackup 0', + 'option_desktop_integration 0', + 'option_doc 0', + 'option_file_assocs 0', + 'option_menu_integration 0', + 'option_src 0', + 'option_w32_multi_user 0', + ].join('\n'); + + core.group('Profile', async () => core.info(profile)); + + const dest = path.join( + await fs.mkdtemp( + path.join(process.env['RUNNER_TEMP'] ?? os.tmpdir(), 'setup-texlive-'), + ), + 'texlive.profile', + ); + await fs.writeFile(dest, profile); + core.debug(`${dest} created`); return dest; - }); + } + + static executable(version: Version, platform: NodeJS.Platform): string { + const ext = `${Number(version) > 2012 ? '-windows' : ''}.bat`; + return `install-tl${platform === 'win32' ? ext : ''}`; + } } -async function createProfile( +async function patch( version: Version, - prefix: string, -): Promise { - const tlmgr = new Manager(version, prefix); - const texmf = tlmgr.conf(); - const adjustrepo = version === LATEST_VERSION ? 1 : 0; - - const profile = ` - TEXDIR ${texmf.texdir} - TEXMFLOCAL ${texmf.local} - TEXMFSYSCONFIG ${texmf.sysconfig} - TEXMFSYSVAR ${texmf.sysvar} - selected_scheme scheme-infraonly - instopt_adjustrepo ${adjustrepo} - tlpdbopt_autobackup 0 - tlpdbopt_desktop_integration 0 - tlpdbopt_file_assocs 0 - tlpdbopt_install_docfiles 0 - tlpdbopt_install_srcfiles 0 - tlpdbopt_w32_multi_user 0 - ` - .trim() - .split(/\n\s*/) - .join('\n'); - - core.startGroup('Profile'); - core.info(profile); - core.endGroup(); - - const tmp = process.env['RUNNER_TEMP'] ?? os.tmpdir(); - const dest = path.join( - await fs.mkdtemp(path.join(tmp, 'setup-texlive-')), - 'texlive.profile', - ); - await fs.writeFile(dest, profile); - core.debug(`${dest} created`); - - return dest; + platform: NodeJS.Platform, + texdir: string, +): Promise { + const update = async (filename: string, map: (content: string) => string) => { + return fs.writeFile(filename, map(await fs.readFile(filename, 'utf8'))); + }; + /** + * Prevent `install-tl(-windows).bat` from being stopped by `pause`. + */ + if (platform === 'win32') { + try { + await update( + path.join(texdir, InstallTL.executable(version, platform)), + (content) => content.replace(/\bpause(?: Done)?\b/gm, ''), + ); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((error as any)?.code !== 'ENOENT') { + throw error; + } + } + } + /** + * Fix a syntax error in `tlpkg/TeXLive/TLWinGoo.pm`. + */ + if (['2009', '2010'].includes(version)) { + await update( + path.join(texdir, 'tlpkg', 'TeXLive', 'TLWinGoo.pm'), + (content) => { + return content.replace( + /foreach \$p qw\((.*)\)/, + 'foreach $$p (qw($1))', + ); + }, + ); + } + /** + * Define Code Page 65001 as an alias for UTF-8 on Windows. + * @see {@link https://github.com/dankogai/p5-encode/issues/37} + */ + if (platform === 'win32' && version === '2015') { + await update( + path.join(texdir, 'tlpkg', 'tlperl', 'lib', 'Encode', 'Alias.pm'), + (content) => { + return content.replace( + '# utf8 is blessed :)', + `define_alias(qr/cp65001/i => '"utf-8-strict"');`, + ); + }, + ); + } + /** + * Make it possible to use `\` as a directory separator on Windows. + */ + if (platform === 'win32' && Number(version) < 2019) { + await update( + path.join(texdir, 'tlpkg', 'TeXLive', 'TLUtils.pm'), + (content) => { + return content.replace( + String.raw`split (/\//, $tree)`, + String.raw`split (/[\/\\]/, $tree)`, + ); + }, + ); + } + /** + * Add support for macOS 11.x. + */ + if (platform === 'darwin' && ['2017', '2018', '2019'].includes(version)) { + await update( + path.join(texdir, 'tlpkg', 'TeXLive', 'TLUtils.pm'), + (content) => { + return content + .replace( + // prettier-ignore + 'if ($os_major != 10)', + 'if ($$os_major < 10)', + ) + .replace( + 'if ($os_minor >= $mactex_darwin)', + 'if ($$os_major >= 11) { $$CPU = "x86_64"; $$OS = "darwin"; } els$&', + ); + }, + ); + } +} + +async function expand(pattern: string): Promise> { + return (await glob.create(pattern, { implicitDescendants: false })).glob(); }