Skip to content

Commit 65362c6

Browse files
author
Kristian Larsson
committed
Add optional OCI output artifacts
Support optional Acton output artifacts fetched from OCI repositories during dependency resolution. Artifacts are keyed by source content hash, interface version, and target tuple. Use valid local cache entries before registry lookups and fall back to source when no matching artifact is available. Add artifact pack, push, and hash commands, local OCI layout support, tests, and guide documentation.
1 parent 836cce0 commit 65362c6

11 files changed

Lines changed: 1157 additions & 77 deletions

File tree

compiler/acton/Main.hs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ main = do
133133
C.CmdOpt gopts (C.PkgUpgrade opts) -> PkgCommands.pkgUpgradeCommand gopts opts
134134
C.CmdOpt gopts C.PkgUpdate -> PkgCommands.pkgUpdateCommand gopts
135135
C.CmdOpt gopts (C.PkgSearch opts) -> PkgCommands.pkgSearchCommand gopts opts
136+
C.CmdOpt gopts (C.Artifact opts) -> artifactCommand gopts opts
136137
C.CmdOpt gopts (C.BuildSpecCmd o) -> buildSpecCommand o
137138
C.CmdOpt gopts (C.Cloud opts) -> undefined
138139
C.CmdOpt gopts (C.Doc opts) -> printDocs gopts opts
@@ -511,6 +512,24 @@ withProjectLockForGen gopts sched gen projDir action =
511512
withProjectLockNotice gopts projDir $
512513
whenCurrentGen sched gen action
513514

515+
artifactCommand :: C.GlobalOptions -> C.ArtifactCommand -> IO ()
516+
artifactCommand gopts cmd =
517+
case cmd of
518+
C.ArtifactHash _ ->
519+
PkgCommands.artifactCommand gopts cmd
520+
_ -> do
521+
let opts = defaultCompileOptions { C.skip_build = True }
522+
sp = Source.diskSourceProvider
523+
paths <- loadProjectPaths opts
524+
let projDir = projPath paths
525+
withProjectLockNotice gopts projDir $ do
526+
unless (C.quiet gopts) $
527+
putStrLn ("Building project in " ++ projDir)
528+
srcFiles <- projectSourceFiles paths
529+
compileFiles sp gopts opts srcFiles True
530+
generateProjectDocIndex sp gopts opts paths srcFiles
531+
PkgCommands.artifactCommand gopts cmd
532+
514533
requireProjectLayout :: Paths -> IO ()
515534
requireProjectLayout paths = do
516535
exists <- doesDirectoryExist (srcDir paths)
@@ -929,7 +948,7 @@ runWatchFile gopts absFile sched runOnce = do
929948
fetchCommand :: C.GlobalOptions -> IO ()
930949
fetchCommand gopts = do
931950
paths <- loadProjectPaths defaultCompileOptions
932-
res <- try (fetchDependencies gopts paths []) :: IO (Either ProjectError ())
951+
res <- try (fetchDependencies gopts paths [] []) :: IO (Either ProjectError ())
933952
case res of
934953
Left (ProjectError msg) -> printErrorAndExit msg
935954
Right () ->
@@ -955,7 +974,7 @@ sigCommand gopts sigOpts = do
955974
rootProj <- normalizePathSafe (projPath paths)
956975
sysAbs <- normalizePathSafe (sysPath paths)
957976
withProjectLockNotice queryGopts rootProj $ do
958-
fetchDependencies queryGopts paths depOverrides
977+
fetchDependencies queryGopts paths depOverrides (C.artifact_repos opts)
959978
projMap <- discoverProjects queryGopts sysAbs rootProj depOverrides
960979
target <- resolveSigTarget opts paths rootProj projMap (C.sigTarget sigOpts)
961980
tyFile <- case target of

compiler/acton/PkgCommands.hs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module PkgCommands
77
, pkgUpgradeCommand
88
, pkgUpdateCommand
99
, pkgSearchCommand
10+
, artifactCommand
1011
, zigPkgAddCommand
1112
, zigPkgRemoveCommand
1213
, PackageEntry(..)
@@ -21,6 +22,7 @@ module PkgCommands
2122

2223
import Prelude hiding (readFile, writeFile)
2324

25+
import qualified Acton.Artifact as Artifact
2426
import qualified Acton.BuildSpec as BuildSpec
2527
import qualified Acton.CommandLineParser as C
2628
import Acton.Compile (loadBuildSpec, throwProjectError)
@@ -30,7 +32,7 @@ import Control.Concurrent (threadDelay)
3032
import Control.Monad (filterM, forM, forM_, unless, when)
3133
import Data.Char (isHexDigit, isSpace)
3234
import Data.Foldable (toList)
33-
import Data.List (dropWhileEnd, isPrefixOf, isSuffixOf, sortOn)
35+
import Data.List (dropWhileEnd, isPrefixOf, isSuffixOf, sort, sortOn)
3436
import Data.List.Split (splitOn)
3537
import Data.Maybe (isJust)
3638
import qualified Data.Map as M
@@ -44,7 +46,7 @@ import Network.HTTP.Client (Manager, Response, httpLbs, parseRequest, requestHea
4446
import Network.HTTP.Client.TLS (newTlsManager)
4547
import Network.HTTP.Types.Header (Header)
4648
import Network.HTTP.Types.Status (statusCode)
47-
import System.Directory (Permissions, canonicalizePath, copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, doesPathExist, getCurrentDirectory, getHomeDirectory, getPermissions, listDirectory, removeFile, setPermissions)
49+
import System.Directory (Permissions, canonicalizePath, copyFile, copyFileWithMetadata, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, doesPathExist, getCurrentDirectory, getHomeDirectory, getPermissions, listDirectory, makeAbsolute, pathIsSymbolicLink, removeFile, setPermissions)
4850
import System.Environment (getExecutablePath, lookupEnv)
4951
import System.Exit (ExitCode(..))
5052
import System.FilePath ((</>), takeDirectory)
@@ -304,6 +306,159 @@ pkgSearchCommand _ opts = do
304306
then putStrLn "No packages matched your search."
305307
else forM_ (sortOn pkgName matched) printPkg
306308

309+
artifactCommand :: C.GlobalOptions -> C.ArtifactCommand -> IO ()
310+
artifactCommand gopts cmd =
311+
case cmd of
312+
C.ArtifactPack opts ->
313+
packActonArtifact gopts (C.artifactPackOutput opts)
314+
C.ArtifactPush opts ->
315+
pushActonArtifact gopts opts
316+
C.ArtifactHash opts ->
317+
printActonArtifactHash (C.artifactHashSourcePath opts)
318+
319+
packActonArtifact :: C.GlobalOptions -> String -> IO ()
320+
packActonArtifact gopts outputArg = do
321+
sourceHash <- computeArtifactSourceHash "."
322+
output <- packActonArtifactTo sourceHash outputArg
323+
unless (C.quiet gopts) $
324+
putStrLn ("Wrote Acton artifact " ++ output)
325+
326+
pushActonArtifact :: C.GlobalOptions -> C.ArtifactPushOptions -> IO ()
327+
pushActonArtifact gopts opts = do
328+
sourceHash <- computeArtifactSourceHash "."
329+
ref0 <- resolveArtifactRef sourceHash
330+
(C.artifactPushRepoUrl opts)
331+
(C.artifactPushArtifactRepo opts)
332+
ref <- absolutizeLocalArtifactRef ref0
333+
withSystemTempDirectory "acton-artifact-push" $ \tmp -> do
334+
let archive = tmp </> Artifact.artifactArchiveFile
335+
_ <- packActonArtifactTo sourceHash archive
336+
runProcessChecked (Just tmp) "oras"
337+
(["push"]
338+
++ Artifact.ociRefOrasOptions ref
339+
++ [ "--artifact-type", Artifact.artifactType
340+
, Artifact.ociRefOrasTarget ref
341+
, Artifact.artifactArchiveFile ++ ":" ++ Artifact.artifactMediaType
342+
])
343+
unless (C.quiet gopts) $
344+
putStrLn ("Pushed Acton artifact " ++ ref)
345+
346+
packActonArtifactTo :: String -> String -> IO FilePath
347+
packActonArtifactTo sourceHash outputArg = do
348+
cwd <- getCurrentDirectory
349+
let output0 = if null outputArg then "out" </> Artifact.artifactArchiveFile else outputArg
350+
createDirectoryIfMissing True (takeDirectory output0)
351+
withSystemTempDirectory "acton-artifact-pack" $ \tmp -> do
352+
Artifact.writeManifest tmp (Artifact.expectedManifest sourceHash)
353+
runProcessChecked Nothing "tar"
354+
[ "-czf", output0
355+
, "-C", tmp, Artifact.artifactManifestFile
356+
, "-C", cwd, "Build.act", "out/types"
357+
]
358+
return output0
359+
360+
printActonArtifactHash :: String -> IO ()
361+
printActonArtifactHash source = do
362+
h <- computeArtifactSourceHash source
363+
putStrLn h
364+
365+
computeArtifactSourceHash :: FilePath -> IO String
366+
computeArtifactSourceHash source = do
367+
isDir <- doesDirectoryExist source
368+
unless isDir $
369+
throwProjectError ("ERROR: Artifact source hash expects a package directory: " ++ source)
370+
zigExe <- getZigExe
371+
withSystemTempDirectory "acton-artifact-source" $ \tmp -> do
372+
let staged = tmp </> "source"
373+
-- TODO(source-hash): Avoid staging a copied package tree here.
374+
--
375+
-- This copy is intentionally simple and conservative, but source hashing is
376+
-- foundational identity machinery and should eventually be computed directly
377+
-- from the package directory. The desired implementation is an Acton-owned
378+
-- package hash walker that follows Zig's package hashing semantics exactly
379+
-- rather than invoking `zig fetch` on a temporary copy.
380+
--
381+
-- Requirements for that replacement:
382+
--
383+
-- * Keep the source boundary independent of git or any other VCS.
384+
-- * Preserve the current Acton package selection rules: include only the
385+
-- canonical package inputs Acton knows about, currently Build.act and
386+
-- src/.
387+
-- * Match Zig's path normalization, directory ordering, file metadata/mode
388+
-- treatment, digest algorithm, and final package-hash text encoding.
389+
-- * Keep symlink behavior explicit. Today symlinks are rejected rather than
390+
-- guessed; any future support must match Zig and have test coverage.
391+
-- * Add stable fixture tests that compare Acton's in-place implementation
392+
-- with `zig fetch` for representative package trees before switching over.
393+
--
394+
-- Until then, copying to a clean temp directory keeps generated build output
395+
-- out of the hash while delegating the actual hash algorithm to Zig.
396+
copyCanonicalSource source staged
397+
requireRightWith "ERROR: Failed to compute source hash: " =<< zigFetchHash zigExe staged
398+
399+
copyCanonicalSource :: FilePath -> FilePath -> IO ()
400+
copyCanonicalSource src dst = do
401+
copyRequiredSourceInput src dst "Build.act"
402+
copyRequiredSourceInput src dst "src"
403+
404+
copyRequiredSourceInput :: FilePath -> FilePath -> FilePath -> IO ()
405+
copyRequiredSourceInput src dst name = do
406+
let srcEntry = src </> name
407+
dstEntry = dst </> name
408+
exists <- doesPathExist srcEntry
409+
unless exists $
410+
throwProjectError ("ERROR: Artifact source hash missing package input: " ++ srcEntry)
411+
copyCanonicalSourceInput srcEntry dstEntry
412+
413+
copyCanonicalSourceInput :: FilePath -> FilePath -> IO ()
414+
copyCanonicalSourceInput src dst = do
415+
isSymlink <- pathIsSymbolicLink src
416+
when isSymlink $
417+
throwProjectError ("ERROR: Artifact source hash does not support symbolic links: " ++ src)
418+
isDir <- doesDirectoryExist src
419+
if isDir
420+
then do
421+
createDirectoryIfMissing True dst
422+
entries <- sort <$> listDirectory src
423+
forM_ entries $ \entry ->
424+
copyCanonicalSourceInput (src </> entry) (dst </> entry)
425+
else do
426+
createDirectoryIfMissing True (takeDirectory dst)
427+
copyFileWithMetadata src dst
428+
429+
absolutizeLocalArtifactRef :: String -> IO String
430+
absolutizeLocalArtifactRef ref
431+
| Artifact.ociRefIsLocal ref =
432+
case splitLocalOciTarget (Artifact.ociRefOrasTarget ref) of
433+
Just (path, tag) -> do
434+
path' <- makeAbsolute path
435+
return ("oci-layout://" ++ path' ++ ":" ++ tag)
436+
Nothing -> return ref
437+
| otherwise = return ref
438+
439+
splitLocalOciTarget :: String -> Maybe (FilePath, String)
440+
splitLocalOciTarget target =
441+
case break (== ':') (reverse target) of
442+
(revTag, ':' : revPath)
443+
| not (null revTag) && not (null revPath) ->
444+
Just (reverse revPath, reverse revTag)
445+
_ -> Nothing
446+
447+
resolveArtifactRef :: String -> String -> String -> IO String
448+
resolveArtifactRef sourceHash repoUrl artifactRepo
449+
| not (null artifactRepo) =
450+
case Artifact.ociRefForRepository artifactRepo sourceHash of
451+
Just ref -> return ref
452+
Nothing ->
453+
throwProjectError ("ERROR: Invalid OCI artifact repository " ++ artifactRepo)
454+
| not (null repoUrl) =
455+
case Artifact.deriveOciRef repoUrl sourceHash of
456+
Just ref -> return ref
457+
Nothing ->
458+
throwProjectError ("ERROR: Could not derive OCI artifact ref from " ++ repoUrl)
459+
| otherwise =
460+
throwProjectError "ERROR: Specify --artifact-repo or --repo-url for artifact push"
461+
307462
zigPkgAddCommand :: C.GlobalOptions -> C.ZigPkgAddOptions -> IO ()
308463
zigPkgAddCommand _ opts = do
309464
let depName = C.zigPkgAddName opts

0 commit comments

Comments
 (0)