What is this for?
This tool is:
- A showcase of something that should be impossible.
- A surprisingly practical tool for making installers/bootstrapping scripts.
This tool converts existing scripts into a universally runnable versions of themselves (one generated file).
- That one generated file runs out-of-the-box for every major OS (Windows, MacOS, and Linux). No āfirst install curlā or āWindows users do this, Linux users do thisā. One file that runs every time everywhere.
- That file is readable and editable (not mangled)
- That file is able to import/use any JavaScript package because all imports are auto installed (no package json or npm install necessary)
- That file, even with imports, does not modify the userās environment (e.g. no side effects)
When executed, the file auto-downloads a specific version of Deno (you pick the version) to an isolated folder, and then runs itself using that exact version of Deno. This makes for extremely consistent/reproducible script behavior across machines. To use npm packages, just import a url with the following format import thing from "https://esm.sh/NPM_PACKAGE_NAME@VERSION". You can also import your own code directly from github import thing from "https://esm.sh/gh/GITHUB_USERNAME/REPO@BRANCH_OR_TAG/path/to/your/code.js. Not only is the import auto downloaded, its cached (courtesy of Deno and Esm.sh) which makes re-runs of the same script very fast.
How the hell is this possible?
This is only possible because of some rare builtin tools that allow for a single file to be valid bash, and valid powershell, AND valid JavaScript (based on this StackOverflow answer Is it possible to write one script that runs in bash/shell and PowerShell?)
How do I make my own universal installer script?
- Write a Deno script, lets call it
your_script.js
console.log("Hello World")- Test it with deno.
# install deno
irm https://deno.land/install.ps1 | iex || curl -fsSL https://deno.land/install.sh | sh
# make sure your script runs normally
deno run --allow-all ./your_script.js- Install this tool (universify) so it can modify your scripts to make them portable
deno install -n uni -Afgr https://raw.githubusercontent.com/jeff-hykin/universify/master/main/universify.js- Use it:
# basic example
uni ./your_script.js
# if you have a particular version of deno you want to use, include it as the second argument
uni ./your_script.js 1.44.4
# if you want to add a deno-run argument like --no-npm do it like this:
uni ./your_script.js --add-arg '--no-npm' --add-arg '--unstable'- Profit
Typing
./your_script(if thats the name of your script) will now run your script! Even if you uninstalled Deno!There are some things to discuss though:
- On Linux/Mac and other half-decent operating systems supported by Deno (including Arm Linux) there is no catch.
- On Windows there is one catch; a fresh Windows install will block execution of all powershell scripts by default.
Set-ExecutionPolicy unrestrictedwill need to be run in an admin terminal before powershell scripts can be executed. After that, it follows the same process as the other operating systems (downloads the specific version of Deno if needed, and executes itself using that version).
Two files are generated, but one is just a symlink, the other is the ārealā file. We can get away with a single file with some compromises:
- Technically
your_script.ps1is the only file needed. Typing./your_script.ps1on any OS will execute correctly.
However, I find the.ps1ugly. And the.ps1is only needed for Windows. The other file (the symlink) is what makes it possible to do./your_scripton all systems:- On Windows, if the file is called
./your_script.ps1then typing./your_scriptwill run it - On Linux/Mac
./your_scriptruns the symlink, which effectively just runsyour_script.ps1.
- On Windows, if the file is called
- If you donāt care about Windows support, delete the non-ps1 file (the symlink), and then remove the
.ps1from the remaining file.
- Technically
How can something be valid Powershell, Bash, and Deno all at the same time? (Polyglot program)
I wrote out an explanation here that covers the basics, and it was fairly straightforward to add support for JavaScript on top of Bash/Powershell. In particular, I just took the official Deno install script and compressed it to fit inline at the top of a file.
What does universify do to the file? (TLDR)
If youāve run the example (./your_script.js), you can manually repeat the steps in 30 seconds on another file.
- Copy the first three lines at the top of
./your_script.ps1, and put it at the top of./your_other_script.js - Copy the last line (a JS comment) from
./your_script.ps1and add it as the last line to./your_other_script.js - Duplicate
./your_other_script.jsgive it a.ps1extension, give it executable permission, and voila.
Thats it! Or at least for 99.9% of programs ./your_other_script.ps1 will now run everywhere. The real heavy lifting is those first three lines (the compressed deno installer). You can modify the deno version thatās in those first three lines, and modify the deno run arguments. If you change the deno run arguments (ex: āno-lock) then make sure to change the arguments everywhere, as there is some (necessary) duplication.
I got really tired of doing that by hand so I made universify to automate the process.
What about the other 0.1% of programs? Well if your code contains #> (even inside a JS comment) then universify will escape it. If youāre doing things manually, then youāll have to escape it yourself to make the script work on Windows.
How do I verify this isnāt malicious?
Glad you asked! The largest step is verifying that those first three lines (the deno installer). Hereās a guide:
- Installer verification
- Thereās four stages. Stage 1 is āliterally the installer straight from deno officialā and it gradually becomes stage 4 āa js function that generates a compressed modified installerā
- stage one is under
./main/1_deno_installer.sh- verify it by comparing it to the official Deno install script https://deno.land/install.sh. Note it does change over time, so my version will lag behind.
- Then compare that to
./main/2_readable.ps1- Read the commits to see explanations of changes (it has to be changed manually)
- ex:
- We donāt want to affect the userās system, so the PATH modification that deno usually performs is commented out
- The deno-version is supplied (instead of grabbing latest)
- A bunch of semicolons need to be added so that the newlines can be removed in the next stage
- The install location is changed from
$HOME/.deno/to$HOME/.deno/${version}/(again we donāt want to affect the userās system deno) - If curl doesnāt exist, we fall back on wget (some systems donāt have curl)
- If unzip doesnāt exist, we try to talk to the user and install it for them (some Linux systems donāt start with unzip)
- Once
./main/2_readable.ps1is verified, look at./run/readable_to_inline.js. It- reads stage 2
- deletes comments
- deletes newlines
- generates
./main/3_inlined.ps1which is the shell + powershell aspect - puts all that into a JavaScript string
- makes a JavaScript function that accepts the deno version and the args (like
--allow-allor--unstable) - puts that JavaScript code inside of
./main/4_inlined.js
- Once those have been verified, open up
universify-api.js(which imports./main/4_inlined.js) - Finally
universify.jsimportsenhanceScriptfromuniversify-api.js
- Verify the main JavaScript
- Look at
universify-api.js(no permissions needed by that code) - Look at
universify.js(needs file permissions because its the CLI script)
- Look at
- Bundle
universify.js, and inspect the dependencies- GoodJs is a permissionless/frontend utility library I maintain
- FileSystem is a quality of life wrapper around Denoās file system and path. Iād like to remove it from universify to make universify easier to verify, but thatās future work for me.
- Footnote:
- Deno didnāt always have official arm64 support
- Older versions of universify got arm64 support using LukeChannings script
- Now universify (as of v1.0.0.5) uses the official deno arm64 installer
Can I generate these files client-side in a browser?
Not sure why you would, but actually yes you can! There is a pure-function API to enable this functionality from within any JavaScript runtime.
import { enhanceScript } from "https://raw.githubusercontent.com/jeff-hykin/universify/master/main/universify-api.js"
const { newContents, symlinkPath, normalPath, ps1Path } = enhanceScript({
filePath: "./my_cli_scipt.js",
jsFileContent: `console.log("Hello World")`,
denoVersion: "1.44.4",
additionalArgs: [ "--no-npm", "--unstable" ],
additionalArgsForUnix: [ ],
additionalArgsForWindows: [ ],
baseArgs: [ "--quiet", "-A", "--no-lock", ],
disableUrlRun: false,
noPs1: false,
})
Deno.writeTextFileSync(ps1Path, newContents)
Deno.symlinkSync(symlinkPath, normalPath)