fix: support symlinked appimage launches

This commit is contained in:
lucaronin
2026-05-13 16:06:57 +02:00
parent 9be428aad3
commit 41f0e00cfb
7 changed files with 427 additions and 6 deletions

View File

@@ -325,6 +325,9 @@ jobs:
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Prepare AppImage symlink launcher patch
run: node scripts/appimage-launcher-tools.mjs prepare-plugin
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
@@ -346,8 +349,11 @@ jobs:
- name: Validate Linux bundles
run: |
shopt -s nullglob
installers=(
appimages=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
)
installers=(
"${appimages[@]}"
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
)
@@ -356,6 +362,10 @@ jobs:
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#appimages[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage bundle."
exit 1
fi
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
exit 1
@@ -364,6 +374,7 @@ jobs:
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
node scripts/appimage-launcher-tools.mjs validate-appimages "${appimages[@]}"
- name: Upload Linux bundles
uses: actions/upload-artifact@v4

View File

@@ -381,6 +381,9 @@ jobs:
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Prepare AppImage symlink launcher patch
run: node scripts/appimage-launcher-tools.mjs prepare-plugin
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
@@ -402,8 +405,11 @@ jobs:
- name: Validate Linux bundles
run: |
shopt -s nullglob
installers=(
appimages=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
)
installers=(
"${appimages[@]}"
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
)
@@ -412,6 +418,10 @@ jobs:
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#appimages[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage bundle."
exit 1
fi
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
exit 1
@@ -420,6 +430,7 @@ jobs:
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
node scripts/appimage-launcher-tools.mjs validate-appimages "${appimages[@]}"
- name: Upload Linux bundles
uses: actions/upload-artifact@v4

View File

@@ -905,6 +905,6 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
- **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events.
### CI/CD
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. macOS release assets use `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, Linux x86_64 `.deb` / AppImage artifacts, and a static public download page that starts the selected installer without replacing the page with a blank download navigation. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. Stable macOS DMG/updater assets use the same `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release.
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. The Linux job pre-seeds Tauri's AppImage output plugin with `scripts/appimage-launcher-tools.mjs` so linuxdeploy's generated AppRun wrapper follows `$0` before taking `dirname`, then extracts the sealed AppImage launcher to validate symlink-safe direct, absolute-symlink, and relative-symlink launch support. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. macOS release assets use `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, Linux x86_64 `.deb` / `.rpm` / AppImage artifacts, and a static public download page that starts the selected installer without replacing the page with a blank download navigation. The Linux job uses the same AppImage symlink-safe launcher patch and sealed-AppImage validation as alpha releases. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. Stable macOS DMG/updater assets use the same `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release.
- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed.

View File

@@ -922,6 +922,11 @@ push to main
→ build-windows job:
→ pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis
→ upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles
→ build-linux job:
→ pnpm install, prepare AppImage symlink-safe launcher patch, stamp version
→ tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
→ extract the sealed AppImage AppRun and verify linuxdeploy resolves $0 before dirname
→ upload .deb, .rpm, .AppImage, and signed Linux updater bundles
→ release job:
→ generate alpha-latest.json with darwin-aarch64, darwin-x86_64, Linux, and Windows updater URLs
→ publish GitHub prerelease alpha-vYYYY.M.D-alpha.NNNN named Tolaria Alpha YYYY.M.D.N
@@ -944,8 +949,10 @@ push stable-vYYYY.M.D tag
→ pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin
→ upload signed Apple Silicon and Intel .app.tar.gz + .sig and .dmg artifacts named Tolaria_<version>_macOS_Silicon and Tolaria_<version>_macOS_Intel
→ build-linux job:
→ pnpm install, stamp version, tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
upload .deb, .AppImage, and signed Linux updater bundles
→ pnpm install, prepare AppImage symlink-safe launcher patch, stamp version
tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
→ extract the sealed AppImage AppRun and verify linuxdeploy resolves $0 before dirname
→ upload .deb, .rpm, .AppImage, and signed Linux updater bundles
→ build-windows job:
→ pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis
→ upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles
@@ -961,6 +968,8 @@ push stable-vYYYY.M.D tag
→ deploy to gh-pages
```
Linux AppImage release jobs run `scripts/appimage-launcher-tools.mjs prepare-plugin` before Tauri packaging. The script pre-seeds Tauri's Linux tools cache with a small `linuxdeploy-plugin-appimage` shim. When linuxdeploy is about to seal the AppImage, that shim patches linuxdeploy's generated GTK AppRun wrapper from `dirname "$0"`-then-`readlink -f` to `readlink -f "$0"`-then-`dirname`, preserving direct launches while allowing absolute and relative symlinks such as `/usr/local/bin/tolaria -> Tolaria.AppImage`. The validation step then extracts `AppRun` from every produced AppImage and fails the release if the symlink-safe resolver is missing.
### Versioning
- Stable promotions use git tags in the form `stable-vYYYY.M.D` and stamp the technical version `YYYY.M.D`.

View File

@@ -45,6 +45,20 @@ WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/u
If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning.
### Linux AppImage packaging checks
Linux release CI prepares Tauri's AppImage tools cache before `pnpm tauri build`:
```bash
node scripts/appimage-launcher-tools.mjs prepare-plugin
```
That step patches linuxdeploy's generated GTK `AppRun` wrapper before the AppImage is sealed so direct launches, absolute symlinks, and relative symlinks resolve the real AppRun path before choosing its directory. After build, CI extracts every produced AppImage launcher and verifies the symlink-safe resolver:
```bash
node scripts/appimage-launcher-tools.mjs validate-appimages src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
```
## Quick Start
```bash

View File

@@ -0,0 +1,262 @@
import { spawnSync } from 'node:child_process'
import { error as logError, log } from 'node:console'
import { existsSync } from 'node:fs'
import {
chmod,
mkdir,
mkdtemp,
readFile,
rename,
rm,
writeFile,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
export const BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE =
'this_dir="$(readlink -f "$(dirname "$0")")"'
export const FIXED_LINUXDEPLOY_APPRUN_DIR_LINE =
'this_dir="$(dirname "$(readlink -f "$0")")"'
export const APPIMAGE_PLUGIN_WRAPPER_NAME = 'linuxdeploy-plugin-appimage.AppImage'
export const REAL_APPIMAGE_PLUGIN_NAME = 'linuxdeploy-plugin-appimage.real.AppImage'
export const DEFAULT_APPIMAGE_PLUGIN_URL =
'https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage'
const WRAPPER_MARKER = 'Tolaria AppImage symlink launcher shim'
export function tauriToolsCacheDir(env = process.env) {
if (env.TOLARIA_TAURI_TOOLS_DIR) {
return resolve(env.TOLARIA_TAURI_TOOLS_DIR)
}
if (env.XDG_CACHE_HOME) {
return resolve(env.XDG_CACHE_HOME, 'tauri')
}
if (!env.HOME) {
throw new Error('HOME or XDG_CACHE_HOME is required to locate the Tauri tools cache')
}
return resolve(env.HOME, '.cache', 'tauri')
}
export function patchAppRunText(text) {
if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
return {
changed: true,
text: text.replaceAll(
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
),
}
}
return { changed: false, text }
}
export function assertSymlinkSafeAppRunText(text, label = 'AppRun') {
if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
throw new Error(`${label} still resolves dirname before following AppRun symlinks`)
}
if (!text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE)) {
throw new Error(`${label} is missing the symlink-safe AppRun directory resolver`)
}
}
export function appImagePluginWrapperSource({
pluginUrl = DEFAULT_APPIMAGE_PLUGIN_URL,
} = {}) {
return `#!/usr/bin/env bash
set -euo pipefail
# ${WRAPPER_MARKER}
PLUGIN_URL="\${TOLARIA_APPIMAGE_PLUGIN_URL:-${pluginUrl}}"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
REAL_PLUGIN="\${TOLARIA_APPIMAGE_REAL_PLUGIN:-"$SCRIPT_DIR/${REAL_APPIMAGE_PLUGIN_NAME}"}"
appdir_from_args() {
local previous=""
for arg in "$@"; do
if [ "$previous" = "--appdir" ]; then
printf '%s\\n' "$arg"
return 0
fi
case "$arg" in
--appdir=*)
printf '%s\\n' "\${arg#--appdir=}"
return 0
;;
esac
previous="$arg"
done
}
download_real_plugin() {
if [ -x "$REAL_PLUGIN" ]; then
return 0
fi
local tmp_plugin="$REAL_PLUGIN.tmp.$$"
rm -f "$tmp_plugin"
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$tmp_plugin" "$PLUGIN_URL"
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$tmp_plugin" "$PLUGIN_URL"
else
echo "curl or wget is required to fetch the real linuxdeploy AppImage output plugin" >&2
return 1
fi
chmod +x "$tmp_plugin"
mv "$tmp_plugin" "$REAL_PLUGIN"
}
patch_apprun() {
local appdir="\${APPDIR:-}"
if [ -z "$appdir" ]; then
appdir="$(appdir_from_args "$@" || true)"
fi
if [ -z "$appdir" ] || [ ! -f "$appdir/AppRun" ]; then
return 0
fi
python3 - "$appdir/AppRun" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
broken = 'this_dir="$(readlink -f "$(dirname "$0")")"'
fixed = 'this_dir="$(dirname "$(readlink -f "$0")")"'
text = path.read_text(encoding="utf-8")
if broken in text:
path.write_text(text.replace(broken, fixed), encoding="utf-8")
print(f"Patched linuxdeploy AppRun symlink resolution in {path}", file=sys.stderr)
elif fixed in text:
pass
elif "autogenerated by linuxdeploy" in text and "AppRun.wrapped" in text:
raise SystemExit(f"{path} is a linuxdeploy wrapper but does not contain the expected AppRun resolver")
PY
}
download_real_plugin
patch_apprun "$@"
exec "$REAL_PLUGIN" "$@"
`
}
export async function preparePluginWrapper({
env = process.env,
toolsDir = tauriToolsCacheDir(env),
} = {}) {
await mkdir(toolsDir, { recursive: true })
const wrapperPath = join(toolsDir, APPIMAGE_PLUGIN_WRAPPER_NAME)
const realPluginPath = join(toolsDir, REAL_APPIMAGE_PLUGIN_NAME)
if (existsSync(wrapperPath) && !existsSync(realPluginPath)) {
const existing = await readFile(wrapperPath, 'utf8').catch(() => '')
if (!existing.includes(WRAPPER_MARKER)) {
await rename(wrapperPath, realPluginPath)
}
}
await writeFile(wrapperPath, appImagePluginWrapperSource(), 'utf8')
await chmod(wrapperPath, 0o755)
return { realPluginPath, wrapperPath }
}
export async function validateAppRunFile(path) {
const text = await readFile(path, 'utf8')
assertSymlinkSafeAppRunText(text, path)
}
export async function validateAppImages(paths) {
if (paths.length === 0) {
throw new Error('At least one AppImage path is required for launcher validation')
}
for (const appImage of paths.map((path) => resolve(path))) {
const tempDir = await mkdtemp(join(tmpdir(), 'tolaria-appimage-'))
try {
const result = spawnSync(appImage, ['--appimage-extract', 'AppRun'], {
cwd: tempDir,
encoding: 'utf8',
})
if (result.status !== 0) {
throw new Error(
[
`Failed to extract AppRun from ${appImage}`,
result.stdout.trim(),
result.stderr.trim(),
]
.filter(Boolean)
.join('\n'),
)
}
await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun'))
} finally {
await rm(tempDir, { recursive: true, force: true })
}
}
}
async function preparePluginCommand() {
const { wrapperPath, realPluginPath } = await preparePluginWrapper()
log(`Prepared ${wrapperPath}`)
log(`Real plugin cache: ${realPluginPath}`)
}
async function validateAppRunFilesCommand(paths) {
for (const path of paths) {
await validateAppRunFile(path)
log(`Validated ${path}`)
}
}
async function validateAppImagesCommand(paths) {
await validateAppImages(paths)
for (const path of paths) {
log(`Validated AppImage launcher in ${path}`)
}
}
const COMMANDS = new Map([
['prepare-plugin', preparePluginCommand],
['validate-apprun-file', validateAppRunFilesCommand],
['validate-appimages', validateAppImagesCommand],
])
function usage() {
return 'Usage: node scripts/appimage-launcher-tools.mjs prepare-plugin | validate-apprun-file <AppRun...> | validate-appimages <AppImage...>'
}
async function main() {
const [command, ...args] = process.argv.slice(2)
const handler = COMMANDS.get(command)
if (!handler) {
throw new Error(usage())
}
await handler(args)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
logError(error.message)
process.exit(1)
})
}

View File

@@ -0,0 +1,114 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { existsSync, realpathSync } from 'node:fs'
import {
chmod,
mkdir,
mkdtemp,
readFile,
symlink,
writeFile,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import process from 'node:process'
import test from 'node:test'
import {
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
appImagePluginWrapperSource,
patchAppRunText,
} from './appimage-launcher-tools.mjs'
function brokenResolverDir(invokedPath) {
return realpathSync(dirname(invokedPath))
}
function fixedResolverDir(invokedPath) {
return dirname(realpathSync(invokedPath))
}
test('patches linuxdeploy AppRun wrapper to resolve the invoked path before dirname', () => {
const original = [
'#! /usr/bin/env bash',
'# autogenerated by linuxdeploy',
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
'exec "$this_dir"/AppRun.wrapped "$@"',
].join('\n')
const patched = patchAppRunText(original)
assert.equal(patched.changed, true)
assert.equal(patched.text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
assert.equal(patched.text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
})
test('fixed resolver follows absolute and relative symlinks before choosing AppDir', async () => {
const root = await mkdtemp(join(tmpdir(), 'tolaria-apprun-resolver-'))
const appDir = join(root, 'Tolaria.AppDir')
const binDir = join(root, 'bin')
const relativeDir = join(root, 'relative-bin')
const appRun = join(appDir, 'AppRun')
await mkdir(appDir)
await mkdir(binDir)
await mkdir(relativeDir)
await writeFile(appRun, '#! /usr/bin/env bash\n', 'utf8')
const absoluteSymlink = join(binDir, 'tolaria')
const relativeSymlink = join(relativeDir, 'tolaria')
await symlink(appRun, absoluteSymlink)
await symlink(`../${basename(appDir)}/AppRun`, relativeSymlink)
assert.equal(brokenResolverDir(absoluteSymlink), realpathSync(binDir))
assert.equal(fixedResolverDir(absoluteSymlink), realpathSync(appDir))
assert.equal(brokenResolverDir(relativeSymlink), realpathSync(relativeDir))
assert.equal(fixedResolverDir(relativeSymlink), realpathSync(appDir))
})
test('plugin wrapper patches AppRun before delegating to the real output plugin', async () => {
const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-plugin-'))
const appDir = join(root, 'Tolaria.AppDir')
const appRun = join(appDir, 'AppRun')
const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage')
const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage')
const pluginMarker = join(root, 'plugin-ran')
await mkdir(appDir)
await writeFile(
appRun,
[
'#! /usr/bin/env bash',
'# autogenerated by linuxdeploy',
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
'exec "$this_dir"/AppRun.wrapped "$@"',
].join('\n'),
'utf8',
)
await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8')
await chmod(wrapper, 0o755)
await writeFile(
realPlugin,
`#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`,
'utf8',
)
await chmod(realPlugin, 0o755)
const result = spawnSync(wrapper, [], {
encoding: 'utf8',
env: {
...process.env,
APPDIR: appDir,
TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin,
},
})
assert.equal(result.status, 0, result.stderr)
assert.equal(existsSync(pluginMarker), true)
const patched = await readFile(appRun, 'utf8')
assert.equal(patched.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
assert.equal(patched.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
})