@@ -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
2223import Prelude hiding (readFile , writeFile )
2324
25+ import qualified Acton.Artifact as Artifact
2426import qualified Acton.BuildSpec as BuildSpec
2527import qualified Acton.CommandLineParser as C
2628import Acton.Compile (loadBuildSpec , throwProjectError )
@@ -30,7 +32,7 @@ import Control.Concurrent (threadDelay)
3032import Control.Monad (filterM , forM , forM_ , unless , when )
3133import Data.Char (isHexDigit , isSpace )
3234import Data.Foldable (toList )
33- import Data.List (dropWhileEnd , isPrefixOf , isSuffixOf , sortOn )
35+ import Data.List (dropWhileEnd , isPrefixOf , isSuffixOf , sort , sortOn )
3436import Data.List.Split (splitOn )
3537import Data.Maybe (isJust )
3638import qualified Data.Map as M
@@ -44,7 +46,7 @@ import Network.HTTP.Client (Manager, Response, httpLbs, parseRequest, requestHea
4446import Network.HTTP.Client.TLS (newTlsManager )
4547import Network.HTTP.Types.Header (Header )
4648import 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 )
4850import System.Environment (getExecutablePath , lookupEnv )
4951import System.Exit (ExitCode (.. ))
5052import 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+
307462zigPkgAddCommand :: C. GlobalOptions -> C. ZigPkgAddOptions -> IO ()
308463zigPkgAddCommand _ opts = do
309464 let depName = C. zigPkgAddName opts
0 commit comments