How do you make standalone programs in Julia?

I’m not talking about standalone executables and PackageCompiler, but rather about actual Julia code. It seems like all official documentation and unofficial tooling (eg. Revise) are focused on writing functions inside of modules. But what is the convention for writing actual programs in Julia, not just packages? Something that you can basically just run with julia file.jl?

I’m interested in both the development workflow (like how to use Revise if my code is not in a module) and the actual usage of the program (how do I run code which has a Package.toml file with dependencies).

Is this not something that people do, and instead I should write all of my programs as packages with modules? How woud I run them in that case?

3 Likes

Organize your code, into packages. Then the “actual program” can be just, e.g.:

using A, B, C
A.run(B.c, C.p())

I have this script where most of the processing is done inside of the MenuMaker.jl module, and the AutoMenu.jl module just prints the results. However, Revise does not automatically update the module definition when I run that top-level code. Here is a simpler example:


src/Program.jl:

include("Extra.jl")
using .Extra
f() |> println

src/Extra.jl:

module Extra
export f
f() = 1
end

If I change the definition of the function to f() = 2, running the third line of the Program.jl file in a REPL with Revise still prints out 1.


Also, in your example, you have

using A, B, C

But wouldn’t that have to be

include("A.jl")
include("B.jl")
include("C.jl")
using .A, .B, .C

?

For revise to track individual files you need to use includet where the final t stands for “track”.

However it is not clear to me what you mean by

running the third line of the Program.jl file in a REPL with Revise still prints out 1.

If you are inside VSCode and just reevaluate that line, then using includet will do precisely what you want. If you run the whole thing again via julia src/Program.jl then include should be sufficient.

Thanks, includet does work. However, wouldn’t that mean that the program can no longer be run by users who don’t have Revise set up?

You’re conflating separate problems, initially you were talking about “actual programs”, so a standalone script, for which Revise is not relevant. Now you mention REPL usage-related issues. So it’s not clear to me what the actual problem here is.

No, look up packages. Once a package is dev-ed or add-ed, that’s how you use it.

Not sure what this means. What is your goal, anyway?

Some of the (mutually alternative) things you could do:

  1. Organize your code into packages, as I said at first.

  2. Use Revise (includet) while developing

  3. Just run include again after you modify a file. You might get a warning about module redefinition, but that’s OK if you’re developing.

1 Like

I need to use Revise and REPL while developing the script, but then run it from the terminal without the REPL

Yes, that is how I’d use third-party packages. But what about my own code which is a part of my program but is defined in a different file?

My goal is to be able to interactively test and run parts of my program while developing it without having to reload anything, but after I’ve finished writing code I should be able to just run it from the terminal like you’d do with code written in Python or Rust or Go or whatever other language

2 Likes

I found this post which is pretty much what I need

However, it doesn’t explain how to properly work with multi-file projects in this way

What do you mean by “packages”? Does this mean that I should create a separate Project.toml file for every single module in my program?

I think that you will find the development of an application much easier if you put your code in modules, each one for a specific purpose, and then instead of using these modules with the Julia native facilities utilise the FromFile.jl package.

1 Like

Thanks, this seems like a great tool, and it automatically uses Revise when it’s available! I’ll try using that

1 Like

This is the conflation I mentioned. IMO it would make much more sense and simplify things to think of it as developing modules (hopefully organized into packages, assuming that suits you), which you would of course do with the help of the REPL, and perhaps (VS) Code or something else utilizing a language server, and then, after the modules are done, the script would be just a one-liner. I.e., if your code is all moduled-up, everything is easier, because when you change some code you just need to include("MainModule.jl") again. Or use Revise, and then you don’t even have to do that.

Create a package skeleton simply by:

using PkgTemplates
generate()

The generate() call will start an interactive process within the REPL where you can select various options, override defaults, etc.

Then put your code into the package’s src directory, and include it from src/PackageName.jl.

EDIT: forgot to say: after this you also need to do dev /path/to/PackageName from the Pkg REPL (or call the equivalent Pkg function, if that is your preference). You could also add your package directory, instead of dev-ing it, but dev is more convenient for an actively and locally developed package because it makes Julia re-precompile the package when there are changes. I believe dev is also preferable when using Revise.

Just be aware of (pre)compilation costs. Julia precompiles packages, which could help avoid the compilation time overhead when starting your script. Some use the PackageCompiler package so they would be able to distribute their program more conveniently. You might need to invest some effort into reducing startup costs, if that matters.

No. Each package contains some, main, module, named the same as the package. But this, main, module may contain other modules. What I prefer to do is have my src/PackageName.jl look like this:

module PackageName
include("SubModule1.jl")
include("SubModule2.jl")
include("SubModule3.jl")
end

And there should probably, as a matter of style, be no other includes anywhere in src. Especially while you’re still a beginner. This is just my style, though, alternatively you could adopt some other style, or even use a third-party package like FromFile for organizing your package. Just be aware that FromFile seems to cause issues for IDEs.

2 Likes

Thanks. Can you look at my script that I linked earlier? Would you suggest moving the contents of src/AutoMenu.jl into a module as well?

There is no official way to do this, except just making a script that imports and runs the code. Notably, this doesn’t manage the script’s environment, or add it to the shell’s PATH. Hopefully, Julia will gain the ability to do this in the future.
The third-party package Comonicon can create “command line interface” packages that, when built, creates a script that runs in the right environment and parses the command line arguments.

Yeah. Applying my advice to your files:

Firstly

At the very least, move everything into modules until the main script is just a one-liner. This would mean moving total_nutrition into a module, and putting the code below total_nutrition into functions and moving it into a module. Finally, have one, main (but not Main :laughing:) module that includes all the other ones. The goal is to have all types and functions defined within modules, so you could easily redefine them with a single include without having to restart the REPL. Even Revise has limitations like being unable to redefine types, which are overcome with having the type definition within a module (because modules can be redefined). For example, this works:

julia> module M
         struct S end
       end
Main.M

julia> M.S()
Main.M.S()

julia> module M
         struct S
           f::Int
         end
       end
WARNING: replacing module M.
Main.M

julia> M.S(3)
Main.M.S(3)

But, this does not:

julia> struct S end

julia> S()
S()

julia> struct S
         f::Int
       end
ERROR: invalid redefinition of constant Main.S
Stacktrace:
 [1] top-level scope
   @ REPL[7]:1

I think you already understand this, but for completeness: I recommend only having types, functions, methods and constants in your modules. So no variable state, and no directly executed code, with rare exceptions.

One of the exceptions is forcing precompilation of some method: instead of explicitly calling precompile, just having some function calls before the very end of a module of yours should ensure the method to be (pre)compiled.

Note that Julia does a lot of precompilation for packages by default, but it may sometimes be necessary to force precompilation of some method with certain argument types, if Julia isn’t able to guess the relevant argument types.

Secondly

After doing the above, you could also try making the above “main” module into a package or applying PackageCompiler.

PS: I made an edit to my previous comment

1 Like

Julia 1.11 (future)

First, I should mention that this is about to change. In Julia 1.11, a @main macro will be available.

That will look like the following. It looks a strange for backwards compatability reasons.

function (@main)(ARGS)
    println("Hello World")
end

Julia 1.9 (present)

The documentation currently provides the following incantation to detect if we are invoking a file as a main program.

if abspath(PROGRAM_FILE) == @__FILE__
    main()
end

You can put the above in a script.

What I usually do at the moment is embed this into the __init__() function of a driver module or package.

module Driver
    using PrecompileTools

    # Modify DRIVER_FILE to point your preferred entry point file
    const DRIVER_FILE = @__FILE__
    function __init__()
        if abspath(PROGRAM_FILE) == DRIVER_FILE
            main(ARGS)
        end
    end

    function main(ARGS::Vector{String})
        # ...
    end
    
    @setup_workload begin
        precompile_args = String[]
        @compile_workload begin
            main(precompile_args)
        end
    end
end

This allows me to then invoke this either by doing

  • julia Driver.jl or
  • julia -e "using Driver; Driver.main()".

Sometimes I will create a script that just contains

using Driver
Driver.main(ARGS)

or

using Driver
# Here DRIVER_FILE would be set to the location of this file
# so __init__() will invoke main(ARGS) automatically
4 Likes