Search code examples
deno

Determine if running uncompiled .ts script or compiled Deno executable


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:

  1. detect if mainModule and execPath are the same (other than the .ts extension)
Deno.mainModule === path.toFileUrl(Deno.execPath()) + '.ts'
  1. detect if the base filename of the 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?


Solution

  • 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.