-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Code loading: add support for "portable scripts". #59982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
f4374bd to
ae11fab
Compare
|
What will happen here if there is only a |
|
Yeah, that's what the
question is about. In order to run the script you at least need to do a resolve it to have manifest data available so you have some concrete versions to load. Maybe julia should do that automatically if it tries to run a portable script without manifest information. But then where should the result be stored? inline or in some hashed directory somewhere? Also, when running a portable script julia should maybe disable the global env from the load path by default? |
|
Oops sorry, I missed you listing that. Yeah, what I was picturing was that we could would have a folder for script environments in Maybe there's a better way to handle that. |
Only manifest files, right? If you don't even have an inline project section, then you are just a normal julia file, or? |
|
I was imagining that project info from the script would get copied there, and if there is a manifest it also gets copied. But maybe that's not necessary and you don't need copies, just writing out a manifest if it's not embedded. |
We just read it inline now, so no need for that really. What should the |
|
In the silly little implementation I played with recently, I was just replacing the path separators in the absolute path with underscores. But there's probably a bunch of reasons why that's a bad idea. |
2af2ff2 to
979f6ce
Compare
|
Could the key for Also it is worth to add that if manifest were to added in the script itself that would make editing such scripts manually by hand much less pleasant. It doesn’t seem like users would want such a level of reproducibility in practice where compat bounds would suffice. |
I thought about that but then you get collisions if you have different scripts that happen to get the same project content.
I disagree, I think you would want to send someone a file and have them be able to reproduce stuff? |
|
Wouldn't interpreting the compat bounds as exact versions with which to resolve produce the same |
|
I don't really want to have a discussion here on how the resolver works. The feature can be made to support both inline and outline manifest info, so not sure what there is to discuss more about this. JuliaLang/Pkg.jl#4479 is a slightly WIP follow-up to this from the pkg side. With that, you can do: and the file updates during pkg operations. |
|
One preference from me: I think it would be nice to put/read the manifest from the bottom, so if you open the script and start looking at it you don't see a few hundred lines of TOML up front. |
|
Yeah, I also got to that conclusion. But the project data should still be on top? |
|
Regarding storing the manifest inline or not, I think a key in the project section could determine that. |
On one hand, splitting it does seem a bit odd, but having project info at the top does feel right to me somehow.
I'm guessing there may be some scripts which are a bit "package-like" in that they have compat bounds on packages, and it's fine for those to be resolved when run. In this way, having the manifest section be optional seems nice. |
|
Actually for the outline manifest we can use the |
979f6ce to
9c3d598
Compare
|
Ok, so I have made the following changes (mostly in Pkg).
A UUID feels a bit excessive in the path... |
How about julia> let f="path/to/myscript.jl"; first(splitext(basename(f))) * '_' * string(hash(f), base=62) end
"myscript_Hkm5zPmRBLp"A 64-bit discriminator seems plenty to me, with ~10k scripts there's a ~1 in a trillion chance of a hash collision. Edit: I remembered case-insensitive filesystems exist. Make that |
|
Question: What do we expect It may not make a lot of sense, but I'm sure that we'll see people try it sooner or later. My instinctive feeling is that doing anything other than using the project/manifest embedded in |
|
Yes (and it should also be what it does now). Should add a test :) |
|
I dont think hashing only by path is good because you get a collision if you then move the script and create a new one in the same place with the same name. Since the path to the manifest is stored in the script itself it doesn't really need to have anything to do with the path (which is why I just grabbed for a uuid). |
|
Hey! This looks awesome, very cool to see this getting standardized! Later in the process, I would like to make sure that this can be made compatible with Pluto's format, so that you can activate and run Pluto notebooks in standalone Julia :) @KristofferC asked me to share some experiences from Pluto (which already has a similar feature by default for notebook files), so here you go: General reactionAmong Pluto's users (education, Julia end-users), people really appreciate this feature and it makes reproducibility much more accessible. It makes it much easier to share your work reliably with new Julia users, which is a huge win. Pluto enables this by default (when creating a notebook, and when opening a notebook). For this PR I would also suggest enabling it by default when opening a file with embedded project data, or at least showing a hint. SurprisesEmbedding the Project+Manifest also turned out less amazing than I thought. The main unexpected issues are: Julia versionsIn practice, the manifest is only useful with the same Julia version, and it is pretty common to change Julia versions. (E.g. when sharing work with someone else, or when opening old work.) We made https://github.com/JuliaPluto/GracefulPkg.jl to still get some value out of unresolvable environments in Pluto, but for base Julia you might want to address this. I would be curious to hear your ideas for this issue, and maybe we can share a solution. Setup timesPackage setup times (install+precomp) are long in Julia, and they get longer with every release. Cache reuse is minimal across environments in practice, which means that each new script launch will probably trigger its own lengthy setup process. Recent Julia releases seem more optimised for the "big global env", rather than many specific environments. This is very relevant in Pluto, where you often share your work, sometimes many notebooks. But if you use this feature less frequently, or only for personal work, then cache hit rates will be high. |
I agree with this. The "competition" has it easier in this case because they often don't use the language itself to launch a script (which immediately ties you to that version of the language), but instead have a launcher one abstraction level above, which can also decide what julia version to use. E.g., in We could have something like
Even for Pkg Apps I feel the pain of the non-flexibility of having the launcher of the app not being able to choose julia version. (cc @davidanthoff) |
FWIW, I implemented this feature in Juliaup ~1y ago (in the |
|
The actual Not something that we can do immediately, but it is feasible. |
Okay, but what we want to do is to choose what julia itself to use, which this doesn't seem to help with.
For reference: JuliaLang/juliaup#10, JuliaLang/juliaup#1095. I think it would be cool to take that further, especially for the case of portable scripts and Julia apps (installed via Pkg). |
So do I. At some point though, it's worth being realistic about how many projects I'm trying to make progress with at once. I might take a look at this again late 2026/early 2027, but at moment I've got a small pile of things I'm similarly interested in that have much nicer git histories. |
|
I'll try to get JuliaLang/juliaup#1095 off the ground again. I think we need that plus auto-instantiation and auto-precompilation (we have the latter) for |
|
Other than tests, there's one feature that I wanted to add to JuliaLang/juliaup#1095 that bears mentioning: using |
9c3d598 to
ab11e23
Compare
|
I haven't had time to read through or try this PR in detail yet but just wanted to point out a package I wrote a short while ago which does pretty much the same thing: https://github.com/cjdoris/SelfContainedScripts.jl It's cool that it may be baked in to Julia and not require an external package, although I was pleasantly surprised at how ergonomically it could be done as a package. |
|
Cool! I think "first class" support, where you can use the package manager, etc, and drive it like a normal environment, and then run it with |
ab11e23 to
96be9e5
Compare
These are jl files that have project and manifest embedded inside them, similar to how Pluto notebooks have done for quite a while. The delimiters are `#!manifest begin` and `#!manifest end` (analogous for project data) and the content can either have single line comments or be inside a multi line comment. When the active project is set to a portable script the project and manifest data will be read from the inlined toml data. Starting julia with a file (`julia file.jl`) will set the file as the active project if julia detects that it is a portable script.
96be9e5 to
e679ccc
Compare
|
Couple of thoughts:
|
|
as an alternative to comments, rust's cargo-script feature seems to be going for frontmatter. They put the lockfile in their build outputs iiuc. Not sure that would work for us but wanted to share. https://github.com/rust-lang/rfcs/blob/master/text/3502-cargo-script.md |
|
Regarding 2., I think it'd be a readability disaster to demand that the project and manifest be at the start of the file. If scanning time of big files is a concern though, we could have a token at the start of the file like e.g. or whatever to signify that there is a project / manifest somewhere that needs to be scanned for. Regarding 3. I think a multiline comment version is nicer to read and write. |
That, but it also seems very weird to me that a random comment somewhere in the middle of the file determines how it should be loaded.
Then why not the proposed just looks extremely ugly. (Though I sill prefer the single-comment version, esp since these are mostly machine written and even if not, IDEs will definitely insert those characters for you). |
I'd argue the manifest should be put specifically at the end of the file. |
I think it'd be reasonable to mandate that the project be at the beginning of the file (since that's usually what you care about seeing and editing and it's not that big) and that the manifest should be at the end of the file with no code following (because you probably won't scroll past it and you want to avoid some trickster - or an LLM putting some surprise extra code there). |
|
Indeed, that's the project/manifest split I had in mind in my earlier comments. |
|
One thought I had was if (by luck) all of the TOML spec can be parsed by the Julia parser and then we could use some Requiring parsing support like in the Rust frontmatter suggestions feels a little bit too "intrusive" to me and would be annoying that you couldn't parse newer files with older julia etc. Something like https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata feels more apt to me. I'm happy to restrict things initially (like not having support for multi-line comments) |
What happnes if you put a snippet of code after the manifest data? Does the script all of a sudden become non-portable and will use your default environment when running? Feels like a bit of a fot gun. |
The presence of |
👍 |
|
I forgot, regarding
I'd say initially no. I am not a fan of versioned manifest because the only reason normal manifests don't work in 99.99% of the cases is due to us looking up stdlib info from the manifest when it can arbitrarily change when changing julia versions. We could have chosen to look up this information from the stdlib project files in which case everything would be fine. |
Just want to say that even if the project block is missing you would still want to consider the script as "portable" if you start with |
|
Also, I wonder if a portable script should run with a restrictive load path by default (not have the global environment in there). It isn't very much portable if you rely on the global env... |
Sure |
May have an opt-in in for that in the Project.toml instead? It seems weird to special case this - the same applies to a Project.toml next to the script. |
How about a string macro? ? Could also have a |
|
I'm now thinking that most of the decisions about portable scripts you want to take before executing the file and also if the manifest section is towards the end I'm not sure you will have macro expanded that before the code runs. And you also want the detection to be easy for other tools (like juliaup) so I don't think you get away from raw string processing to parse the inline content. And at that point, maybe magic comments are OK... |
These are jl files that have project and manifest embedded inside them, similar to how Pluto notebooks have done for quite a while.
The delimiters are
#!manifest beginand#!manifest end(analogous for the project data), and the content can either have single-line comments or be inside a multi-line comment.When the active project is set to a portable script, the project and manifest data will be read from the inlined toml data.
Starting julia with a file (
julia file.jl) will set the file as the active project if julia detects that it is a portable script.The tests and the parser for the inline toml data were written by Claude 🤖 .
I still have to finish the Pkg part (edit JuliaLang/Pkg.jl#4479) that writes these files, so this cannot be tested "end-to-end" right now. However, if one creates a portable script manually like this (rename to .jl)
portable_script.txt, we can check that it works via:
where we can see that the manifest information inside the portable script is used.
Invitation to bike shedding:
Pkg.resolve()? Put it inline or in some temp environment?.jl) to have semantic meaning for the code loading?In addition, I think we need a
--instantiateflag or something in Julia that checks if all the packages are available for the scripts and if not, side-loads Pkg to download them. Maybe that should even be automatic for portable scripts. I think this feature has been requested on the Pkg repo. Being able to get a file and run it in "one shot" is quite tempting to support.Previous work:
https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies
https://internals.rust-lang.org/t/pre-rfc-cargo-script-for-everyone/18639