Skip to content

Instantly share code, notes, and snippets.

@neon-sunset
Last active December 15, 2024 21:10
Show Gist options
  • Save neon-sunset/028937d82f2adaa6c1b93899e358e1c0 to your computer and use it in GitHub Desktop.
Save neon-sunset/028937d82f2adaa6c1b93899e358e1c0 to your computer and use it in GitHub Desktop.
Quick workflow for using F# interactive to build small native console applications
  1. Get .NET SDK with sudo apt install dotnet9 (or dotnet-sdk-9.0), brew install dotnet for macOS
  2. Get FSharpPacker tool with dotnet tool install -g --allow-roll-forward FSharpPacker
  3. Make an F# interactive script file (e.g. copy the phash.fsx below)
  4. Compile it with fspack {your-script.fsx} -f net9.0 -o {destination} --aot
    (in this example: fspack phash.fsx -f net9.0 -o . --aot), note that it will take some time to do so for the first time - .NET needs to fetch IL AOT Compiler from Nuget
  5. Profit! You have compiled an F# script to a native binary
  6. (Optional) If you add fspk.fish, the process is simplified to fspk {my-script}.fsx!

Note 1: if you are not using macOS or FreeBSD, give https://github.com/ieviev/fflat a try which can produce even smaller binaries

Note 2: if you do not need native binaries for quick startup and portability, you can just execute F# scripts with dotnet fsi {script file}, if you want to build a full application, you can always e.g. dotnet new console --language F#

function fspk
set name (string replace '.fsx' '' $argv[1])
fspack $argv[1] -f net9.0 --aot -o . \
/p:InvariantGlobalization=true \
/p:IlcInstructionSet=native \
/p:IlcFoldIdenticalMethodBodies=true \
/p:OptimizationPreference=Size &&
rm -rf ./$name.dbg ./$name.dSYM
end
#r "nuget: FSharp.Control.TaskSeq"
#nowarn "760"
open System
open System.Security.Cryptography
open System.IO
open System.Threading
open System.Threading.Channels
open System.Threading.Tasks
open FSharp.Control
// 1.. for fspack, 2.. for fsi to skip program name
let args = Environment.GetCommandLineArgs()[1..]
if args.Length < 2 then
printfn "Usage: phash <path> <pattern>"; exit 0
let path = args[0].Trim()
let pattern = args[1].Trim()
// Define channel operators for style points :)
// Someone even made an entire Concurrent ML on top of this concept... (Hopac)
let (<--) (ch: Channel<_>) msg = ch.Writer.TryWrite msg |> ignore
let (->>) (ch: Channel<_>) f = ch.Reader.ReadAllAsync() |> TaskSeq.iter f
let println s = Console.WriteLine(s: string)
// Open a file enumerator
let files = Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories)
let ch = Channel.CreateUnbounded()
// Declare a function to hash a file and write the result to a channel
let hash path ct =
task {
// use binding declares a disposable resource
use file = File.OpenRead path
// let! binding here is C#'s await, it's worth to
// read on F#'s async implementation!
let! hash = SHA256.HashDataAsync(file, ct)
ch <-- (path, Convert.ToHexStringLower hash)
} |> ValueTask
// Read from an asynchronous sequence
let reader = ch ->> fun (path, hash) -> println $"{hash} {path}"
// Start hashing the files in parallel
Parallel.ForEachAsync(files, CancellationToken.None, hash).Wait()
// Signal that we're done writing to the channel
ch.Writer.Complete()
reader.Wait()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment