I want my program to be able to create a symlink to itself. The problem is that I want to distribute it as a compiled executable, but I also want to be able to test it as a script during development with this functionality still in place. So the trick is, what target should the symlink point to?
If it's a script that should be executed directly (the main file has a shebang), then I think that Deno.mainModule
would be the ideal target.
If it's a compiled executable, then I think that Deno.execPath()
would be the ideal target.
The question is, how do I know which is the case? Is there a good way to tell whether or not the program is being run as a compiled executable?
When running an uncompiled script, the two values are:
execPath: "/opt/local/bin/deno"
mainModule: "file:///Users/myname/mycode/myprogram.ts"
When running a compiled executable, the two values are:
execPath: "/Users/myname/mycode/myprogram"
mainModule: "file:///Users/myname/mycode/myprogram.ts"
So, the two best heuristics I can come up with to detect if it's a compiled executable are roughly these:
mainModule
and execPath
are the same (other than the .ts
extension)Deno.mainModule === path.toFileUrl(Deno.execPath()) + '.ts'
execPath
is not 'deno'
path.basename(Deno.execPath()) !== 'deno'
Note: I assume that Windows executables will have a .exe
extension, but I've left that out for simplicity
I like the first one better because it seems more likely to be invariant, but neither seem quite as invariant as I would hope. Is there a better practice for this? Or am I going about this the wrong way altogether?
There's not currently an API for discriminating this, but there's an open GitHub issue on the topic if you'd like to follow its status: denoland/deno#15996 — Runtime API to check whether in self-contained exe. There's also a related PR: denoland/deno#18402 — feat: Add Deno.standalone API
I posted a comment in the original issue describing a workaround, and I'll inline it here as well:
You can switch on the presence of a specific CLI argument to make the determination, and embed that argument into the compiled binary:
example.ts
:import * as path from "https://deno.land/std@0.158.0/path/mod.ts"; const isCompiled = Deno.args.includes("--is_compiled_binary"); const programPath = isCompiled ? Deno.execPath() : path.fromFileUrl(Deno.mainModule); const programDir = path.dirname(programPath); console.log({ isCompiled, programDir, programPath, });
gh-issue-15996 % deno --version deno 1.26.0 (release, x86_64-apple-darwin) v8 10.7.193.3 typescript 4.8.3 gh-issue-15996 % deno run --allow-read --no-prompt example.ts { isCompiled: false, programDir: "/Users/deno/gh-issue-15996", programPath: "/Users/deno/gh-issue-15996/example.ts" } gh-issue-15996 % mkdir binary_dir gh-issue-15996 % deno compile --output=binary_dir/example --allow-read --no-prompt example.ts --is_compiled_binary Compile file:///Users/deno/gh-issue-15996/example.ts Emit binary_dir/example gh-issue-15996 % ./binary_dir/example { isCompiled: true, programDir: "/Users/deno/gh-issue-15996/binary_dir", programPath: "/Users/deno/gh-issue-15996/binary_dir/example" }
Of course, you'll have to account for this potential extra argument if your program already accepts other arguments — and if you're concerned about the target argument being inadvertently used with the not-compiled script (a false positive), just use something unguessable like a UUID in place of the one I used in the example above.