Single-file scripts that download their dependencies
dbohdan.com
dbohdan.com
The smart is of course to review the script and see what its dependencies are.
This is not always straight forward, esp. when it gets recursive.
In that NPM is the worst offender I have tried.
It can easily expand from 10 libraries to hundreds of dependencies (and dependencies of the dependencies)
For deno, as the article points out, it's easy, deno has this feature baked in and we just have to deno run the scripts, similar for go where the imports are pretty much a fully qualified pointer to their source and version.
For python, we have to do a bit of machinery: we parse the AST, look at the imports, use the heuristics that most import names correspond to their pypi package (we maintain also a list of exception-mappings), pip-compile [2] all the imports, and then get a requirement file that we attach to the script, pip-install it before running the script (then we do a LOT of magic to cache the dependencies so one doesn't actually have to install them 99.9% of the time).
Note: if you're interested in a collection of single-file scripts to many APIs, that you can just copy-paste and call the main function of, that's what windmill's hub is for [3]
[1]: https://github.com/windmill-labs/windmill [2]: https://github.com/jazzband/pip-tools [3]: https://hub.windmill.dev/
So it's close but not exactly the same.
Although I'm wondering about how fast it starts on the 2nd run after the dependencies have been download and how it deals with language specify dependencies, e.g. npm/pip packages. Read somewhere thst it can be a pain deal with those in nix.
To answer your questions: On the second run all packages are saved in the local store, so it's super fast. Language specific dependencies are treated like all other Nix packages, so Python dependencies can depend on Node, etc.
There’s no shebang support in nix3 (the new, flakes-native CLI with a single “nix” command). The closest you can get is probably “nix run”—can’t be distilled down to a single file, but can fetch and cache even online Git repos (use “nix registry pin” to freeze the commit hash).
So would you want to fit everything into just flake.nix? I don’t see why. Could you? Yeah, sure: http://ix.io/4ll5 is an example for a single shell script.
This is a pain point, but there appears to be discussions and a PR for Shebang support with flakes: https://github.com/NixOS/nix/pull/5189
2. AppFS appears to be written by a user named rkeene, and I am replying to a comment by rkeene2. I think a disclosure would be helpful when comparing AppFS with other tools.
Additionally, I'm the author of AppFS.net just redirects you to AppFS.rkeene.org -- I'm not sure what kind of disclosure is needed when discussing it in this context though.
Not true. See "Using Nix to run software with no installation steps" https://determinate.systems/posts/nix-run for example.
This feature has been one of the main use cases of Nix.
> Nix has an install step ... AppFS does not... I'm the author of AppFS.net...
I appreciate the disclosure, as it helps readers (like me) to judge the merits of your claims, especially regarding AppFS vs other options, and whether those claims could be biased (due to differences in familiarity, for example).
> I'm not sure what kind of disclosure is needed...
Over Hacker News and other forums, when the author of a tool wants to promote their work (nothing wrong with this, I learnt a lot with such promotions) in the context of competing tools, the following disclosures are common:
1. Disclosure: I wrote AppFS.
2. Author of AppFS here. (Just like what you did.)
3. Shameless plug. (Note: I personally do not see this as a shame, but I just want to give examples of common disclosure styles in such contexsts.)
$ cat /tmp/hello.py
#! /opt/appfs/rkeene.org/python3/platform/latest/bin/python3
print("Hello, world!".rjust(20, "-"))
$ /tmp/hello.py
-------Hello, world!Additionally any external dependencies can be specified by just accessing them the same way you would in `nix-shell`, just using the path instead of installing. For example, if you wanted to use the "awscli" (the only Python package I already created within AppFS) you would do:
$ cat /tmp/aws.py
#! /opt/appfs/rkeene.org/python3/platform/latest/bin/python3
import sys
sys.path.append('/opt/appfs/rkeene.org/aws-cli/platform/latest/lib/python3.6/site-packages')
import awscli
print(awscli)
$ /tmp/aws.py
<module 'awscli' from '/opt/appfs/rkeene.org/aws-cli/platform/latest/lib/python3.6/site-packages/awscli/__init__.py'>
[0] https://browser.appfs.net/rkeene.org/python3/linux-x86_64/3.... #!/usr/bin/env -S dotnet fsi
#r "nuget: Avalonia, 11.0.0-preview4"
#r "nuget: Avalonia.Desktop, 11.0.0-preview4"
#r "nuget: Avalonia.Themes.Fluent, 11.0.0-preview4"
#r "nuget: JaggerJo.Avalonia.FuncUI, 0.6.0-preview4"
open Avalonia
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Hosts
open Avalonia.Layout
open Avalonia.Themes.Fluent
open System
module Main =
let view =
Component(fun ctx ->
DockPanel.create
[ DockPanel.children
[ TextBlock.create
[ TextBlock.fontSize 48.0
TextBlock.verticalAlignment VerticalAlignment.Center
TextBlock.horizontalAlignment HorizontalAlignment.Center
TextBlock.text "Hello world!" ] ] ])
type MainWindow() =
inherit HostWindow()
do
base.Title <- "Hello World!"
base.Height <- 400.0
base.Width <- 400.0
base.Content <- Main.view
type App() =
inherit Application()
override this.Initialize() =
this.Styles.Add(FluentTheme(baseUri = null, Mode = FluentThemeMode.Dark))
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
let mainWindow = MainWindow()
desktopLifetime.MainWindow <- mainWindow
| _ -> ()
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime(Environment.GetCommandLineArgs())Here is a hello world server that fetches all of its own dependencies - just 3 lines!
#r "nuget: Suave, 2.6.2"
open Suave
startWebServer defaultConfig (Successful.OK "Hello World!") require 'bundler/inline'
gemfile do
gem 'httparty'
end
p HTTParty.get("https://acme.com/")It doesn't even need tea itself to run, it'll make a bash script that'll get it too if absent.
(ns malli
(:require [babashka.deps :as deps]))
;; This adds malli (https://github.com/metosin/malli) to babashka's classpath:
(deps/add-deps '{:deps {metosin/malli {:mvn/version "0.10.0"}}})
;; Now we're able to load malli:
(require '[malli.core :as malli])
(prn (malli/validate [:+ :int] [1 2 3])) ;; trueTo run a computation I need code and data to be in the right place. If you analysed the number of add instructions of an average compiled program, I feel we would be surprised how many instructions are actual computations and not copying or moving. I'm excluding address calculations and referring to "add" or "subtract".
Preparing an environment for computation is copying data to the machine and preparing dependencies such as docker, package managers, compilation and other work. It's also moving data between registers in preparation for a calculation. It's similar to a factory processing raw ingredients that need transportation.
How do you represent all this movement and logistics efficiently and untediously? I feel computer science could probably expend some effort on representing logistics efficiently.
Within a codebase there's a arbitrary number mental models that that particular code base uses to solve problems. Perhaps it uses a calling convention, or a REST API or a C FFI API. Or Spring. The problem is ultimately how do I get data from here to there so that I understand it for doing an organised computation on it.
When you go outside that codebase, you switch mental models of how to solve and think of problems. It's similar to switching between writing in Python and then writing commands in bash shell.
It's similar to switching between HTML, CSS and Javascript as a frontend developer. Or embedded C programming or the code of a compiler.
I feel the logistical problem is space or position based, which can also be computed with numbers or vectors as with language models.
How many of the problems that people solve with programs are the same problem, said slightly differently? BSD sockets standardised communication but we have so many solutions to positional+placement logistics: systemd, sysv, Tomcat/application servers, kubernetes, package managers, docker, wasm runtimes, virtual machines, firecracker, OSGI, LXC, processes, threads, databases and so on. Said another way: what is where.
Unfortunately, having been in development from 1977-1988, it was an example of Solving a Problem Too Early.
https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
Combine this with a shebang and you can get a heck of a lot of power into one file.
I'll add Ruby if conflicts are normally impossible. (If conflicts are possible, the method is equivalent to shelling out to the language's package manager at the top of the script before the imports, which you can do in most scripting languages.)
#! /usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 -p python3Packages.requests -p python3Packages.fire
import requests
import fire
class CLI:
def get(self, url):
return requests.get(url).status_code
fire.Fire(CLI)
Run with
chmod +x example.py
./example.py get https://example.comCosmopolitan libc for poly-OS static binaries.
This doesn't seem too different from the old 'setup.py' approach of Python packaging except that the 'one script' has both a 'setup.py' in it and the real script.