Merge pull request 'content addressing' (#174) from content-addressing into main
Reviewed-on: #174
This commit is contained in:
commit
13dd238843
15 changed files with 381 additions and 125 deletions
|
@ -16,6 +16,13 @@ let defer_foreign_keys =
|
||||||
Caqti_type.unit ->. Caqti_type.unit @@
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
"PRAGMA defer_foreign_keys = ON"
|
"PRAGMA defer_foreign_keys = ON"
|
||||||
|
|
||||||
|
let build_artifacts_to_orphan =
|
||||||
|
Builder_db.Rep.id `build ->* Builder_db.Rep.cstruct @@
|
||||||
|
{| SELECT a.sha256 FROM build_artifact a
|
||||||
|
WHERE a.build = ? AND
|
||||||
|
(SELECT COUNT(*) FROM build_artifact a2
|
||||||
|
WHERE a2.sha256 = a.sha256 AND a2.build <> a.build) = 0 |}
|
||||||
|
|
||||||
let connect uri =
|
let connect uri =
|
||||||
let* (module Db : Caqti_blocking.CONNECTION) = Caqti_blocking.connect ~tweaks_version:(1,8) uri in
|
let* (module Db : Caqti_blocking.CONNECTION) = Caqti_blocking.connect ~tweaks_version:(1,8) uri in
|
||||||
let* () = Db.exec defer_foreign_keys () in
|
let* () = Db.exec defer_foreign_keys () in
|
||||||
|
@ -36,6 +43,16 @@ let do_migrate dbpath =
|
||||||
let migrate () dbpath =
|
let migrate () dbpath =
|
||||||
or_die 1 (do_migrate dbpath)
|
or_die 1 (do_migrate dbpath)
|
||||||
|
|
||||||
|
let artifacts_dir datadir = Fpath.(datadir / "_artifacts")
|
||||||
|
let artifact_path sha256 =
|
||||||
|
let (`Hex sha256) = Hex.of_cstruct sha256 in
|
||||||
|
(* NOTE: [sha256] is 64 characters when it's a hex sha256 checksum *)
|
||||||
|
(* NOTE: We add the prefix to reduce the number of files in a directory - a
|
||||||
|
workaround for inferior filesystems. We can easily revert this by changing
|
||||||
|
this function and adding a migration. *)
|
||||||
|
let prefix = String.sub sha256 0 2 in
|
||||||
|
Fpath.(v "_artifacts" / prefix / sha256)
|
||||||
|
|
||||||
let user_mod action dbpath scrypt_n scrypt_r scrypt_p username unrestricted =
|
let user_mod action dbpath scrypt_n scrypt_r scrypt_p username unrestricted =
|
||||||
let scrypt_params = Builder_web_auth.scrypt_params ?scrypt_n ?scrypt_r ?scrypt_p () in
|
let scrypt_params = Builder_web_auth.scrypt_params ?scrypt_n ?scrypt_r ?scrypt_p () in
|
||||||
let r =
|
let r =
|
||||||
|
@ -154,6 +171,18 @@ let job_remove () datadir jobname =
|
||||||
(match Bos.OS.Dir.delete ~recurse:true dir with
|
(match Bos.OS.Dir.delete ~recurse:true dir with
|
||||||
| Ok _ -> ()
|
| Ok _ -> ()
|
||||||
| Error `Msg e -> Logs.warn (fun m -> m "failed to remove build directory %a: %s" Fpath.pp dir e));
|
| Error `Msg e -> Logs.warn (fun m -> m "failed to remove build directory %a: %s" Fpath.pp dir e));
|
||||||
|
let* () =
|
||||||
|
Db.iter_s build_artifacts_to_orphan
|
||||||
|
(fun sha256 ->
|
||||||
|
let p = Fpath.(v datadir // artifact_path sha256) in
|
||||||
|
match Bos.OS.Path.delete p with
|
||||||
|
| Ok () -> Ok ()
|
||||||
|
| Error `Msg e ->
|
||||||
|
Logs.warn (fun m -> m "failed to remove orphan artifact %a: %s"
|
||||||
|
Fpath.pp p e);
|
||||||
|
Ok ())
|
||||||
|
build_id
|
||||||
|
in
|
||||||
let* () = Db.exec Builder_db.Build_artifact.remove_by_build build_id in
|
let* () = Db.exec Builder_db.Build_artifact.remove_by_build build_id in
|
||||||
Db.exec Builder_db.Build.remove build_id)
|
Db.exec Builder_db.Build.remove build_id)
|
||||||
(Ok ())
|
(Ok ())
|
||||||
|
@ -214,12 +243,12 @@ let num_build_artifacts =
|
||||||
Caqti_type.unit ->! Caqti_type.int @@
|
Caqti_type.unit ->! Caqti_type.int @@
|
||||||
"SELECT count(*) FROM build_artifact"
|
"SELECT count(*) FROM build_artifact"
|
||||||
|
|
||||||
let build_artifacts : (unit, string * Uuidm.t * (Fpath.t * Fpath.t * Cstruct.t * int64), [ `One | `Zero | `Many ]) Caqti_request.t =
|
let build_artifacts : (unit, string * Uuidm.t * (Fpath.t * Cstruct.t * int64), [ `One | `Zero | `Many ]) Caqti_request.t =
|
||||||
Caqti_type.unit ->*
|
Caqti_type.unit ->*
|
||||||
Caqti_type.(tup3 string Builder_db.Rep.uuid
|
Caqti_type.(tup3 string Builder_db.Rep.uuid
|
||||||
(tup4 Builder_db.Rep.fpath Builder_db.Rep.fpath Builder_db.Rep.cstruct int64))
|
(tup3 Builder_db.Rep.fpath Builder_db.Rep.cstruct int64))
|
||||||
@@
|
@@
|
||||||
{| SELECT job.name, b.uuid, a.filepath, a.localpath, a.sha256, a.size
|
{| SELECT job.name, b.uuid, a.filepath, a.sha256, a.size
|
||||||
FROM build_artifact a, build b, job
|
FROM build_artifact a, build b, job
|
||||||
WHERE a.build = b.id AND b.job = job.id |}
|
WHERE a.build = b.id AND b.job = job.id |}
|
||||||
|
|
||||||
|
@ -257,35 +286,32 @@ let verify_data_dir () datadir =
|
||||||
let idx = ref 0 in
|
let idx = ref 0 in
|
||||||
fun () -> incr idx; if !idx mod 100 = 0 then Logs.info (fun m -> m "%d" !idx);
|
fun () -> incr idx; if !idx mod 100 = 0 then Logs.info (fun m -> m "%d" !idx);
|
||||||
in
|
in
|
||||||
let verify_job_and_uuid ?fpath job uuid path =
|
let verify_job_and_uuid job uuid path =
|
||||||
match Fpath.segs path with
|
match Fpath.segs path with
|
||||||
| job' :: uuid' :: tl ->
|
| job' :: uuid' :: _tl ->
|
||||||
if String.equal job job' then () else Logs.warn (fun m -> m "job names do not match: %s vs %s" job job');
|
if String.equal job job' then () else Logs.warn (fun m -> m "job names do not match: %s vs %s" job job');
|
||||||
if String.equal (Uuidm.to_string uuid) uuid' then () else Logs.warn (fun m -> m "uuid does not match: %s vs %s" (Uuidm.to_string uuid) uuid');
|
if String.equal (Uuidm.to_string uuid) uuid' then () else Logs.warn (fun m -> m "uuid does not match: %s vs %s" (Uuidm.to_string uuid) uuid');
|
||||||
(match fpath, tl with
|
|
||||||
| None, _ -> ()
|
|
||||||
| Some f, "output" :: tl ->
|
|
||||||
if Fpath.equal (Fpath.v (String.concat "/" tl)) f then
|
|
||||||
()
|
|
||||||
else
|
|
||||||
Logs.err (fun m -> m "path (%a) and fpath (%a) do not match" Fpath.pp path Fpath.pp f)
|
|
||||||
| Some _, _ ->
|
|
||||||
Logs.err (fun m -> m "path is not of form <job>/<uuid>/output/<filename>: %a" Fpath.pp path))
|
|
||||||
| _ -> Logs.err (fun m -> m "path is not of form <job>/<uuid>/...: %a" Fpath.pp path)
|
| _ -> Logs.err (fun m -> m "path is not of form <job>/<uuid>/...: %a" Fpath.pp path)
|
||||||
in
|
in
|
||||||
let* () =
|
let* () =
|
||||||
Db.iter_s build_artifacts (fun (job, uuid, (fpath, lpath, sha, size)) ->
|
Db.iter_s build_artifacts (fun (_job, _uuid, (_fpath, sha256, size)) ->
|
||||||
progress ();
|
progress ();
|
||||||
verify_job_and_uuid ~fpath job uuid lpath;
|
if not (FpathSet.mem (artifact_path sha256) !files_tracked) then
|
||||||
let abs_path = Fpath.(v datadir // lpath) in
|
let abs_path = Fpath.(v datadir // artifact_path sha256) in
|
||||||
(match Bos.OS.File.read abs_path with
|
(match Bos.OS.File.read abs_path with
|
||||||
| Error (`Msg msg) -> Logs.err (fun m -> m "file %a not present: %s" Fpath.pp abs_path msg)
|
| Error (`Msg msg) -> Logs.err (fun m -> m "file %a not present: %s" Fpath.pp abs_path msg)
|
||||||
| Ok data ->
|
| Ok data ->
|
||||||
files_tracked := FpathSet.add lpath !files_tracked;
|
files_tracked := FpathSet.add (artifact_path sha256) !files_tracked;
|
||||||
let s = Int64.of_int (String.length data) in
|
let s = Int64.of_int (String.length data) in
|
||||||
if s <> size then Logs.err (fun m -> m "File %a has different size (in DB %Lu on disk %Lu)" Fpath.pp abs_path size s);
|
if s <> size then Logs.err (fun m -> m "File %a has different size (in DB %Lu on disk %Lu)" Fpath.pp abs_path size s);
|
||||||
let sh = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
let sha256' = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
||||||
if not (Cstruct.equal sha sh) then Logs.err (fun m -> m "File %a has different hash (in DB %a on disk %a" Fpath.pp abs_path Cstruct.hexdump_pp sha Cstruct.hexdump_pp sh)) ;
|
if not (Cstruct.equal sha256 sha256') then
|
||||||
|
Logs.err (fun m -> m "File %a has different hash (in DB %a on disk %a)"
|
||||||
|
Fpath.pp abs_path
|
||||||
|
Hex.pp (Hex.of_cstruct sha256)
|
||||||
|
Hex.pp (Hex.of_cstruct sha256'))) ;
|
||||||
|
Ok ()
|
||||||
|
else
|
||||||
Ok ()
|
Ok ()
|
||||||
) ()
|
) ()
|
||||||
in
|
in
|
||||||
|
@ -460,7 +486,7 @@ module Verify_cache_dir = struct
|
||||||
AND ba_opam_switch.filepath = 'opam-switch'
|
AND ba_opam_switch.filepath = 'opam-switch'
|
||||||
LEFT JOIN build_artifact AS ba_debug_bin ON
|
LEFT JOIN build_artifact AS ba_debug_bin ON
|
||||||
ba_debug_bin.build = b.id
|
ba_debug_bin.build = b.id
|
||||||
AND ba_debug_bin.localpath LIKE '%.debug'
|
AND ba_debug_bin.filepath LIKE '%.debug'
|
||||||
|}
|
|}
|
||||||
|
|
||||||
let check_viz_nonempty ~cachedir ~viz_typ ~hash =
|
let check_viz_nonempty ~cachedir ~viz_typ ~hash =
|
||||||
|
@ -681,9 +707,9 @@ let extract_full () datadir dest uuid =
|
||||||
let out = console_of_string console in
|
let out = console_of_string console in
|
||||||
let* artifacts = Db.collect_list Builder_db.Build_artifact.get_all_by_build build_id in
|
let* artifacts = Db.collect_list Builder_db.Build_artifact.get_all_by_build build_id in
|
||||||
let* data =
|
let* data =
|
||||||
List.fold_left (fun acc (_, { Builder_db.filepath; localpath; _ }) ->
|
List.fold_left (fun acc (_, { Builder_db.filepath; sha256; _ }) ->
|
||||||
let* acc = acc in
|
let* acc = acc in
|
||||||
let* data = Bos.OS.File.read Fpath.(v datadir // localpath) in
|
let* data = Bos.OS.File.read Fpath.(v datadir // artifact_path sha256) in
|
||||||
Ok ((filepath, data) :: acc))
|
Ok ((filepath, data) :: acc))
|
||||||
(Ok [])
|
(Ok [])
|
||||||
artifacts
|
artifacts
|
||||||
|
|
|
@ -180,6 +180,7 @@ let () =
|
||||||
actions (module M20211105);
|
actions (module M20211105);
|
||||||
actions (module M20220509);
|
actions (module M20220509);
|
||||||
actions (module M20230911);
|
actions (module M20230911);
|
||||||
|
actions (module M20230914);
|
||||||
])
|
])
|
||||||
|> Cmd.eval
|
|> Cmd.eval
|
||||||
|> exit
|
|> exit
|
||||||
|
|
162
bin/migrations/m20230914.ml
Normal file
162
bin/migrations/m20230914.ml
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
let new_version = 18L and old_version = 17L
|
||||||
|
and identifier = "2023-09-14"
|
||||||
|
and migrate_doc = "Artifacts are stored content-addressed in the filesystem"
|
||||||
|
and rollback_doc = "Artifacts are stored under their build's job name and uuid"
|
||||||
|
|
||||||
|
open Grej.Syntax
|
||||||
|
|
||||||
|
let new_build_artifact =
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
{| CREATE TABLE new_build_artifact (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
filepath TEXT NOT NULL,
|
||||||
|
sha256 BLOB NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
build INTEGER NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(build) REFERENCES build(id),
|
||||||
|
UNIQUE(build, filepath)
|
||||||
|
)
|
||||||
|
|}
|
||||||
|
|
||||||
|
let old_build_artifact =
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
{| CREATE TABLE new_build_artifact (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
filepath TEXT NOT NULL, -- the path as in the build
|
||||||
|
localpath TEXT NOT NULL, -- local path to the file on disk
|
||||||
|
sha256 BLOB NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
build INTEGER NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(build) REFERENCES build(id),
|
||||||
|
UNIQUE(build, filepath)
|
||||||
|
)
|
||||||
|
|}
|
||||||
|
|
||||||
|
let idx_build_artifact_sha256 =
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)"
|
||||||
|
|
||||||
|
let idx_build_artifact_build =
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
"CREATE INDEX idx_build_artifact_build ON build_artifact(build)"
|
||||||
|
|
||||||
|
let copy_new_build_artifact =
|
||||||
|
Caqti_type.(unit ->. unit) @@
|
||||||
|
{| INSERT INTO new_build_artifact(id, filepath, sha256, size, build)
|
||||||
|
SELECT id, filepath, sha256, size, build
|
||||||
|
FROM build_artifact
|
||||||
|
|}
|
||||||
|
|
||||||
|
let copy_old_build_artifact =
|
||||||
|
Caqti_type.(unit ->. unit) @@
|
||||||
|
{| INSERT INTO new_build_artifact(id, filepath, localpath, sha256, size, build)
|
||||||
|
SELECT a.id, a.filepath,
|
||||||
|
j.name || '/' || b.uuid || '/output/' || a.filepath,
|
||||||
|
a.sha256, a.size, a.build
|
||||||
|
FROM build_artifact a, job j, build b
|
||||||
|
WHERE b.id = a.build AND j.id = b.job
|
||||||
|
|}
|
||||||
|
|
||||||
|
let new_build_artifact_paths =
|
||||||
|
Caqti_type.unit ->* Caqti_type.(tup2 string string) @@
|
||||||
|
{| SELECT localpath, '_artifacts/' || substr(lower(hex(sha256)), 1, 2) || '/' || lower(hex(sha256))
|
||||||
|
FROM build_artifact
|
||||||
|
|}
|
||||||
|
|
||||||
|
let old_build_artifact_paths =
|
||||||
|
Caqti_type.unit ->* Caqti_type.(tup2 string string) @@
|
||||||
|
{| SELECT '_artifacts/' || substr(lower(hex(a.sha256)), 1, 2) || '/' || lower(hex(a.sha256)),
|
||||||
|
j.name || '/' || b.uuid || '/output/' || a.filepath
|
||||||
|
FROM build_artifact a, job j, build b
|
||||||
|
WHERE b.id = a.build AND j.id = b.job
|
||||||
|
|}
|
||||||
|
|
||||||
|
let drop_build_artifact =
|
||||||
|
Caqti_type.(unit ->. unit) @@
|
||||||
|
"DROP TABLE build_artifact"
|
||||||
|
|
||||||
|
let rename_build_artifact =
|
||||||
|
Caqti_type.(unit ->. unit) @@
|
||||||
|
"ALTER TABLE new_build_artifact RENAME TO build_artifact"
|
||||||
|
|
||||||
|
let move_paths ?force datadir (old_path, new_path) =
|
||||||
|
let old_path = Fpath.(datadir // v old_path) and new_path = Fpath.(datadir // v new_path) in
|
||||||
|
let* _created = Bos.OS.Dir.create (Fpath.parent new_path) in
|
||||||
|
Bos.OS.Path.move ?force old_path new_path
|
||||||
|
|
||||||
|
let copy_paths datadir (old_path, new_path) =
|
||||||
|
let old_path = Fpath.(datadir // v old_path) and new_path = Fpath.(datadir // v new_path) in
|
||||||
|
let new_path_tmp = Fpath.(new_path + "tmp") in
|
||||||
|
let* _created = Bos.OS.Dir.create (Fpath.parent new_path) in
|
||||||
|
let cmd = Bos.Cmd.(v "cp" % p old_path % p new_path_tmp) in
|
||||||
|
let* () =
|
||||||
|
match Bos.OS.Cmd.run_status cmd with
|
||||||
|
| Ok `Exited 0 ->
|
||||||
|
Ok ()
|
||||||
|
| Ok status ->
|
||||||
|
let _ = Bos.OS.Path.delete new_path_tmp in
|
||||||
|
Error (`Msg (Fmt.str "cp failed: %a" Bos.OS.Cmd.pp_status status))
|
||||||
|
| Error _ as e ->
|
||||||
|
let _ = Bos.OS.Path.delete new_path_tmp in
|
||||||
|
e
|
||||||
|
in
|
||||||
|
Bos.OS.Path.move ~force:true new_path_tmp new_path
|
||||||
|
|
||||||
|
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
|
||||||
|
let* () = Grej.check_version ~user_version:old_version (module Db) in
|
||||||
|
let* () = Db.exec new_build_artifact () in
|
||||||
|
let* () = Db.exec copy_new_build_artifact () in
|
||||||
|
let* () = Db.iter_s new_build_artifact_paths (move_paths ~force:true datadir) () in
|
||||||
|
let* () = Db.exec drop_build_artifact () in
|
||||||
|
let* () = Db.exec rename_build_artifact () in
|
||||||
|
let* () = Db.exec idx_build_artifact_sha256 () in
|
||||||
|
let* () = Db.exec idx_build_artifact_build () in
|
||||||
|
Db.exec (Grej.set_version new_version) ()
|
||||||
|
|
||||||
|
let rollback datadir (module Db : Caqti_blocking.CONNECTION) =
|
||||||
|
let* () = Grej.check_version ~user_version:new_version (module Db) in
|
||||||
|
let* () = Db.exec old_build_artifact () in
|
||||||
|
let* () = Db.exec copy_old_build_artifact () in
|
||||||
|
let* () = Db.iter_s old_build_artifact_paths (copy_paths datadir) () in
|
||||||
|
let* () =
|
||||||
|
Db.iter_s old_build_artifact_paths
|
||||||
|
(fun (old_path, _new_path) ->
|
||||||
|
Bos.OS.Path.delete Fpath.(datadir // v old_path))
|
||||||
|
()
|
||||||
|
in
|
||||||
|
let* () = Db.exec drop_build_artifact () in
|
||||||
|
let* () = Db.exec rename_build_artifact () in
|
||||||
|
let* () = Db.exec idx_build_artifact_sha256 () in
|
||||||
|
Db.exec (Grej.set_version old_version) ()
|
||||||
|
|
||||||
|
(* migration failed but managed to move *some* files *)
|
||||||
|
let fixup_migrate datadir (module Db : Caqti_blocking.CONNECTION) =
|
||||||
|
let* () = Grej.check_version ~user_version:old_version (module Db) in
|
||||||
|
let* () =
|
||||||
|
Db.iter_s new_build_artifact_paths
|
||||||
|
(fun (old_path, new_path) ->
|
||||||
|
let* old_exists = Bos.OS.Path.exists Fpath.(datadir // v old_path) in
|
||||||
|
let* new_exists = Bos.OS.Path.exists Fpath.(datadir // v new_path) in
|
||||||
|
if new_exists && not old_exists then
|
||||||
|
copy_paths datadir (new_path, old_path)
|
||||||
|
else Ok ())
|
||||||
|
()
|
||||||
|
in
|
||||||
|
Db.iter_s new_build_artifact_paths
|
||||||
|
(fun (_old_path, new_path) ->
|
||||||
|
Bos.OS.Path.delete Fpath.(datadir // v new_path))
|
||||||
|
()
|
||||||
|
|
||||||
|
(* rollback failed but some or all artifacts were copied *)
|
||||||
|
let fixup_rollback datadir (module Db : Caqti_blocking.CONNECTION) =
|
||||||
|
let* () = Grej.check_version ~user_version:new_version (module Db) in
|
||||||
|
Db.iter_s old_build_artifact_paths
|
||||||
|
(fun (old_path, new_path) ->
|
||||||
|
let* old_exists = Bos.OS.Path.exists Fpath.(datadir // v old_path) in
|
||||||
|
if old_exists then
|
||||||
|
Bos.OS.Path.delete Fpath.(datadir // v new_path)
|
||||||
|
else
|
||||||
|
move_paths datadir (new_path, old_path))
|
||||||
|
()
|
|
@ -11,7 +11,6 @@ type 'a id = 'a Rep.id
|
||||||
|
|
||||||
type file = Rep.file = {
|
type file = Rep.file = {
|
||||||
filepath : Fpath.t;
|
filepath : Fpath.t;
|
||||||
localpath : Fpath.t;
|
|
||||||
sha256 : Cstruct.t;
|
sha256 : Cstruct.t;
|
||||||
size : int;
|
size : int;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +139,6 @@ module Build_artifact = struct
|
||||||
{| CREATE TABLE build_artifact (
|
{| CREATE TABLE build_artifact (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
filepath TEXT NOT NULL, -- the path as in the build
|
filepath TEXT NOT NULL, -- the path as in the build
|
||||||
localpath TEXT NOT NULL, -- local path to the file on disk
|
|
||||||
sha256 BLOB NOT NULL,
|
sha256 BLOB NOT NULL,
|
||||||
size INTEGER NOT NULL,
|
size INTEGER NOT NULL,
|
||||||
build INTEGER NOT NULL,
|
build INTEGER NOT NULL,
|
||||||
|
@ -156,13 +154,13 @@ module Build_artifact = struct
|
||||||
|
|
||||||
let get =
|
let get =
|
||||||
id `build_artifact ->! file @@
|
id `build_artifact ->! file @@
|
||||||
{| SELECT filepath, localpath, sha256, size
|
{| SELECT filepath, sha256, size
|
||||||
FROM build_artifact WHERE id = ? |}
|
FROM build_artifact WHERE id = ? |}
|
||||||
|
|
||||||
let get_by_build_uuid =
|
let get_by_build_uuid =
|
||||||
Caqti_type.tup2 uuid fpath ->? Caqti_type.tup2 (id `build_artifact) file @@
|
Caqti_type.tup2 uuid fpath ->? Caqti_type.tup2 (id `build_artifact) file @@
|
||||||
{| SELECT build_artifact.id, build_artifact.filepath,
|
{| SELECT build_artifact.id, build_artifact.filepath,
|
||||||
build_artifact.localpath, build_artifact.sha256, build_artifact.size
|
build_artifact.sha256, build_artifact.size
|
||||||
FROM build_artifact
|
FROM build_artifact
|
||||||
INNER JOIN build ON build.id = build_artifact.build
|
INNER JOIN build ON build.id = build_artifact.build
|
||||||
WHERE build.uuid = ? AND build_artifact.filepath = ?
|
WHERE build.uuid = ? AND build_artifact.filepath = ?
|
||||||
|
@ -170,12 +168,16 @@ module Build_artifact = struct
|
||||||
|
|
||||||
let get_all_by_build =
|
let get_all_by_build =
|
||||||
id `build ->* Caqti_type.(tup2 (id `build_artifact) file) @@
|
id `build ->* Caqti_type.(tup2 (id `build_artifact) file) @@
|
||||||
"SELECT id, filepath, localpath, sha256, size FROM build_artifact WHERE build = ?"
|
"SELECT id, filepath, sha256, size FROM build_artifact WHERE build = ?"
|
||||||
|
|
||||||
|
let exists =
|
||||||
|
cstruct ->! Caqti_type.bool @@
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM build_artifact WHERE sha256 = ?)"
|
||||||
|
|
||||||
let add =
|
let add =
|
||||||
Caqti_type.(tup2 file (id `build)) ->. Caqti_type.unit @@
|
Caqti_type.(tup2 file (id `build)) ->. Caqti_type.unit @@
|
||||||
"INSERT INTO build_artifact (filepath, localpath, sha256, size, build) \
|
"INSERT INTO build_artifact (filepath, sha256, size, build) \
|
||||||
VALUES (?, ?, ?, ?, ?)"
|
VALUES (?, ?, ?, ?)"
|
||||||
|
|
||||||
let remove_by_build =
|
let remove_by_build =
|
||||||
id `build ->. Caqti_type.unit @@
|
id `build ->. Caqti_type.unit @@
|
||||||
|
@ -316,7 +318,7 @@ module Build = struct
|
||||||
b.uuid, b.start_d, b.start_ps, b.finish_d, b.finish_ps,
|
b.uuid, b.start_d, b.start_ps, b.finish_d, b.finish_ps,
|
||||||
b.result_code, b.result_msg, b.console, b.script,
|
b.result_code, b.result_msg, b.console, b.script,
|
||||||
b.platform, b.main_binary, b.input_id, b.user, b.job,
|
b.platform, b.main_binary, b.input_id, b.user, b.job,
|
||||||
a.filepath, a.localpath, a.sha256, a.size
|
a.filepath, a.sha256, a.size
|
||||||
FROM build b, build_artifact a
|
FROM build b, build_artifact a
|
||||||
WHERE b.main_binary = a.id AND b.job = $1 AND b.platform = $2
|
WHERE b.main_binary = a.id AND b.job = $1 AND b.platform = $2
|
||||||
AND b.main_binary IS NOT NULL
|
AND b.main_binary IS NOT NULL
|
||||||
|
@ -442,7 +444,7 @@ module Build = struct
|
||||||
{| SELECT b.uuid, b.start_d, b.start_ps, b.finish_d, b.finish_ps,
|
{| SELECT b.uuid, b.start_d, b.start_ps, b.finish_d, b.finish_ps,
|
||||||
b.result_code, b.result_msg, b.console, b.script,
|
b.result_code, b.result_msg, b.console, b.script,
|
||||||
b.platform, b.main_binary, b.input_id, b.user, b.job,
|
b.platform, b.main_binary, b.input_id, b.user, b.job,
|
||||||
a.filepath, a.localpath, a.sha256, a.size
|
a.filepath, a.sha256, a.size
|
||||||
FROM build_artifact a
|
FROM build_artifact a
|
||||||
INNER JOIN build b ON b.id = a.build
|
INNER JOIN build b ON b.id = a.build
|
||||||
WHERE a.sha256 = ?
|
WHERE a.sha256 = ?
|
||||||
|
@ -593,6 +595,8 @@ let migrate = [
|
||||||
"CREATE INDEX idx_build_main_binary ON build(main_binary)";
|
"CREATE INDEX idx_build_main_binary ON build(main_binary)";
|
||||||
Caqti_type.unit ->. Caqti_type.unit @@
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)";
|
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)";
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
"CREATE INDEX idx_build_artifact_build ON build_artifact(build)";
|
||||||
set_current_version;
|
set_current_version;
|
||||||
set_application_id;
|
set_application_id;
|
||||||
]
|
]
|
||||||
|
@ -606,6 +610,8 @@ let rollback = [
|
||||||
Build.rollback;
|
Build.rollback;
|
||||||
Job.rollback;
|
Job.rollback;
|
||||||
Caqti_type.unit ->. Caqti_type.unit @@
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
|
"DROP INDEX IF EXISTS idx_build_artifact_build";
|
||||||
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
"DROP INDEX IF EXISTS idx_build_artifact_sha256";
|
"DROP INDEX IF EXISTS idx_build_artifact_sha256";
|
||||||
Caqti_type.unit ->. Caqti_type.unit @@
|
Caqti_type.unit ->. Caqti_type.unit @@
|
||||||
"DROP INDEX IF EXISTS idx_build_failed";
|
"DROP INDEX IF EXISTS idx_build_failed";
|
||||||
|
|
|
@ -3,7 +3,6 @@ module Rep : sig
|
||||||
type 'a id
|
type 'a id
|
||||||
type file = {
|
type file = {
|
||||||
filepath : Fpath.t;
|
filepath : Fpath.t;
|
||||||
localpath : Fpath.t;
|
|
||||||
sha256 : Cstruct.t;
|
sha256 : Cstruct.t;
|
||||||
size : int;
|
size : int;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +22,6 @@ type 'a id = 'a Rep.id
|
||||||
|
|
||||||
type file = Rep.file = {
|
type file = Rep.file = {
|
||||||
filepath : Fpath.t;
|
filepath : Fpath.t;
|
||||||
localpath : Fpath.t;
|
|
||||||
sha256 : Cstruct.t;
|
sha256 : Cstruct.t;
|
||||||
size : int;
|
size : int;
|
||||||
}
|
}
|
||||||
|
@ -87,6 +85,7 @@ module Build_artifact : sig
|
||||||
Caqti_request.t
|
Caqti_request.t
|
||||||
val get_all_by_build :
|
val get_all_by_build :
|
||||||
([`build] id, [`build_artifact] id * file, [ `Many | `One | `Zero ]) Caqti_request.t
|
([`build] id, [`build_artifact] id * file, [ `Many | `One | `Zero ]) Caqti_request.t
|
||||||
|
val exists : (Cstruct.t, bool, [ `One ]) Caqti_request.t
|
||||||
val add :
|
val add :
|
||||||
(file * [`build] id, unit, [ `Zero ]) Caqti_request.t
|
(file * [`build] id, unit, [ `Zero ]) Caqti_request.t
|
||||||
val remove_by_build :
|
val remove_by_build :
|
||||||
|
|
|
@ -30,7 +30,6 @@ let id_to_int64 (id : 'a id) : int64 = id
|
||||||
|
|
||||||
type file = {
|
type file = {
|
||||||
filepath : Fpath.t;
|
filepath : Fpath.t;
|
||||||
localpath : Fpath.t;
|
|
||||||
sha256 : Cstruct.t;
|
sha256 : Cstruct.t;
|
||||||
size : int;
|
size : int;
|
||||||
}
|
}
|
||||||
|
@ -63,24 +62,24 @@ let cstruct =
|
||||||
Caqti_type.custom ~encode ~decode Caqti_type.octets
|
Caqti_type.custom ~encode ~decode Caqti_type.octets
|
||||||
|
|
||||||
let file =
|
let file =
|
||||||
let encode { filepath; localpath; sha256; size } =
|
let encode { filepath; sha256; size } =
|
||||||
Ok (filepath, localpath, sha256, size) in
|
Ok (filepath, sha256, size) in
|
||||||
let decode (filepath, localpath, sha256, size) =
|
let decode (filepath, sha256, size) =
|
||||||
Ok { filepath; localpath; sha256; size } in
|
Ok { filepath; sha256; size } in
|
||||||
Caqti_type.custom ~encode ~decode Caqti_type.(tup4 fpath fpath cstruct int)
|
Caqti_type.custom ~encode ~decode Caqti_type.(tup3 fpath cstruct int)
|
||||||
|
|
||||||
let file_opt =
|
let file_opt =
|
||||||
let rep = Caqti_type.(tup4 (option fpath) (option fpath) (option cstruct) (option int)) in
|
let rep = Caqti_type.(tup3 (option fpath) (option cstruct) (option int)) in
|
||||||
let encode = function
|
let encode = function
|
||||||
| Some { filepath; localpath; sha256; size } ->
|
| Some { filepath; sha256; size } ->
|
||||||
Ok (Some filepath, Some localpath, Some sha256, Some size)
|
Ok (Some filepath, Some sha256, Some size)
|
||||||
| None ->
|
| None ->
|
||||||
Ok (None, None, None, None)
|
Ok (None, None, None)
|
||||||
in
|
in
|
||||||
let decode = function
|
let decode = function
|
||||||
| (Some filepath, Some localpath, Some sha256, Some size) ->
|
| (Some filepath, Some sha256, Some size) ->
|
||||||
Ok (Some { filepath; localpath; sha256; size })
|
Ok (Some { filepath; sha256; size })
|
||||||
| (None, None, None, None) ->
|
| (None, None, None) ->
|
||||||
Ok None
|
Ok None
|
||||||
| _ ->
|
| _ ->
|
||||||
(* This should not happen if the database is well-formed *)
|
(* This should not happen if the database is well-formed *)
|
||||||
|
|
|
@ -211,9 +211,9 @@ module Viz_aux = struct
|
||||||
match typ with
|
match typ with
|
||||||
| `Treemap ->
|
| `Treemap ->
|
||||||
let debug_binary =
|
let debug_binary =
|
||||||
let bin = Fpath.base main_binary.localpath in
|
let bin = Fpath.base main_binary.filepath in
|
||||||
List.find_opt
|
List.find_opt
|
||||||
(fun p -> Fpath.(equal (bin + "debug") (base p.localpath)))
|
(fun p -> Fpath.(equal (bin + "debug") (base p.filepath)))
|
||||||
artifacts
|
artifacts
|
||||||
in
|
in
|
||||||
begin
|
begin
|
||||||
|
@ -226,7 +226,7 @@ module Viz_aux = struct
|
||||||
| `Dependencies ->
|
| `Dependencies ->
|
||||||
let opam_switch =
|
let opam_switch =
|
||||||
List.find_opt
|
List.find_opt
|
||||||
(fun p -> Fpath.(equal (v "opam-switch") (base p.localpath)))
|
(fun p -> Fpath.(equal (v "opam-switch") (base p.filepath)))
|
||||||
artifacts
|
artifacts
|
||||||
in
|
in
|
||||||
Model.not_found opam_switch
|
Model.not_found opam_switch
|
||||||
|
@ -435,8 +435,8 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
|
||||||
| _ ->
|
| _ ->
|
||||||
Model.build_artifact_data datadir file
|
Model.build_artifact_data datadir file
|
||||||
|> if_error "Error getting build artifact"
|
|> if_error "Error getting build artifact"
|
||||||
~log:(fun e -> Log.warn (fun m -> m "Error getting build artifact data for file %a in %a: %a"
|
~log:(fun e -> Log.warn (fun m -> m "Error getting build artifact data for file %a: %a"
|
||||||
Fpath.pp file.Builder_db.filepath Fpath.pp file.Builder_db.localpath
|
Fpath.pp file.Builder_db.filepath
|
||||||
pp_error e)) >>= fun data ->
|
pp_error e)) >>= fun data ->
|
||||||
let headers = [
|
let headers = [
|
||||||
"Content-Type", mime_lookup file.Builder_db.filepath;
|
"Content-Type", mime_lookup file.Builder_db.filepath;
|
||||||
|
|
|
@ -67,7 +67,7 @@ let targz_response datadir finish (files : Builder_db.file list) (stream : Dream
|
||||||
in
|
in
|
||||||
Lwt_list.iter_s (fun file ->
|
Lwt_list.iter_s (fun file ->
|
||||||
let hdr = header_of_file finish file in
|
let hdr = header_of_file finish file in
|
||||||
write_block hdr Fpath.(datadir // file.localpath) state)
|
write_block hdr Fpath.(datadir // Model.artifact_path file) state)
|
||||||
files >>= fun () ->
|
files >>= fun () ->
|
||||||
Writer.really_write state Tar.Header.zero_block >>= fun () ->
|
Writer.really_write state Tar.Header.zero_block >>= fun () ->
|
||||||
Writer.really_write state Tar.Header.zero_block >>= fun () ->
|
Writer.really_write state Tar.Header.zero_block >>= fun () ->
|
||||||
|
|
113
lib/model.ml
113
lib/model.ml
|
@ -19,6 +19,14 @@ let not_found = function
|
||||||
| Some v -> Lwt_result.return v
|
| Some v -> Lwt_result.return v
|
||||||
|
|
||||||
let staging datadir = Fpath.(datadir / "_staging")
|
let staging datadir = Fpath.(datadir / "_staging")
|
||||||
|
let artifact_path artifact =
|
||||||
|
let (`Hex sha256) = Hex.of_cstruct artifact.Builder_db.sha256 in
|
||||||
|
(* NOTE: [sha256] is 64 characters when it's a hex sha256 checksum *)
|
||||||
|
(* NOTE: We add the prefix to reduce the number of files in a directory - a
|
||||||
|
workaround for inferior filesystems. We can easily revert this by changing
|
||||||
|
this function and adding a migration. *)
|
||||||
|
let prefix = String.sub sha256 0 2 in
|
||||||
|
Fpath.(v "_artifacts" / prefix / sha256)
|
||||||
|
|
||||||
let read_file datadir filepath =
|
let read_file datadir filepath =
|
||||||
let filepath = Fpath.(datadir // filepath) in
|
let filepath = Fpath.(datadir // filepath) in
|
||||||
|
@ -44,14 +52,14 @@ let build_artifact_by_id id (module Db : CONN) =
|
||||||
Db.find Builder_db.Build_artifact.get id
|
Db.find Builder_db.Build_artifact.get id
|
||||||
|
|
||||||
let build_artifact_data datadir file =
|
let build_artifact_data datadir file =
|
||||||
read_file datadir file.Builder_db.localpath
|
read_file datadir (artifact_path file)
|
||||||
|
|
||||||
let build_artifacts build (module Db : CONN) =
|
let build_artifacts build (module Db : CONN) =
|
||||||
Db.collect_list Builder_db.Build_artifact.get_all_by_build build >|=
|
Db.collect_list Builder_db.Build_artifact.get_all_by_build build >|=
|
||||||
List.map snd
|
List.map snd
|
||||||
|
|
||||||
let solo5_manifest datadir file =
|
let solo5_manifest datadir file =
|
||||||
let buf = Owee_buf.map_binary Fpath.(to_string (datadir // file.Builder_db.localpath)) in
|
let buf = Owee_buf.map_binary Fpath.(to_string (datadir // artifact_path file)) in
|
||||||
Solo5_elftool.query_manifest buf |> Result.to_option
|
Solo5_elftool.query_manifest buf |> Result.to_option
|
||||||
|
|
||||||
let platforms_of_job id (module Db : CONN) =
|
let platforms_of_job id (module Db : CONN) =
|
||||||
|
@ -196,11 +204,11 @@ let cleanup_staging datadir (module Db : Caqti_lwt.CONNECTION) =
|
||||||
(cleanup_staged staged))
|
(cleanup_staged staged))
|
||||||
stageds
|
stageds
|
||||||
|
|
||||||
let save file data =
|
let save path data =
|
||||||
let open Lwt.Infix in
|
let open Lwt.Infix in
|
||||||
Lwt.catch
|
Lwt.catch
|
||||||
(fun () ->
|
(fun () ->
|
||||||
Lwt_io.open_file ~mode:Lwt_io.Output (Fpath.to_string file) >>= fun oc ->
|
Lwt_io.open_file ~mode:Lwt_io.Output (Fpath.to_string path) >>= fun oc ->
|
||||||
Lwt_io.write oc data >>= fun () ->
|
Lwt_io.write oc data >>= fun () ->
|
||||||
Lwt_io.close oc
|
Lwt_io.close oc
|
||||||
|> Lwt_result.ok)
|
|> Lwt_result.ok)
|
||||||
|
@ -209,33 +217,29 @@ let save file data =
|
||||||
Lwt_result.fail (`Msg (Unix.error_message e))
|
Lwt_result.fail (`Msg (Unix.error_message e))
|
||||||
| e -> Lwt.fail e)
|
| e -> Lwt.fail e)
|
||||||
|
|
||||||
let save_file dir staging (filepath, data) =
|
let save_artifacts staging artifacts =
|
||||||
let size = String.length data in
|
|
||||||
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
|
||||||
let localpath = Fpath.append dir filepath in
|
|
||||||
let destpath = Fpath.append staging filepath in
|
|
||||||
Lwt_result.lift (Bos.OS.Dir.create (Fpath.parent destpath)) >>= fun _ ->
|
|
||||||
save destpath data >|= fun () ->
|
|
||||||
{ Builder_db.filepath; localpath; sha256; size }
|
|
||||||
|
|
||||||
let save_files dir staging files =
|
|
||||||
List.fold_left
|
List.fold_left
|
||||||
(fun r file ->
|
(fun r (file, data) ->
|
||||||
r >>= fun acc ->
|
r >>= fun () ->
|
||||||
save_file dir staging file >>= fun file ->
|
let (`Hex sha256) = Hex.of_cstruct file.Builder_db.sha256 in
|
||||||
Lwt_result.return (file :: acc))
|
let destpath = Fpath.(staging / sha256) in
|
||||||
(Lwt_result.return [])
|
save destpath data)
|
||||||
files
|
(Lwt_result.return ())
|
||||||
|
artifacts
|
||||||
|
|
||||||
let save_all staging_dir (job : Builder.script_job) uuid artifacts =
|
let commit_files datadir staging_dir job_name uuid artifacts =
|
||||||
let build_dir = Fpath.(v job.Builder.name / Uuidm.to_string uuid) in
|
(* First we move the artifacts *)
|
||||||
let output_dir = Fpath.(build_dir / "output")
|
List.fold_left
|
||||||
and staging_output_dir = Fpath.(staging_dir / "output") in
|
(fun r artifact ->
|
||||||
Lwt.return (Bos.OS.Dir.create staging_output_dir) >>= fun _ ->
|
r >>= fun () ->
|
||||||
save_files output_dir staging_output_dir artifacts >>= fun artifacts ->
|
let (`Hex sha256) = Hex.of_cstruct artifact.Builder_db.sha256 in
|
||||||
Lwt_result.return artifacts
|
let src = Fpath.(staging_dir / sha256) in
|
||||||
|
let dest = Fpath.(datadir // artifact_path artifact) in
|
||||||
let commit_files datadir staging_dir job_name uuid =
|
Lwt.return (Bos.OS.Dir.create (Fpath.parent dest)) >>= fun _created ->
|
||||||
|
Lwt.return (Bos.OS.Path.move ~force:true src dest))
|
||||||
|
(Lwt_result.return ())
|
||||||
|
artifacts >>= fun () ->
|
||||||
|
(* Now the staging dir only contains script & console *)
|
||||||
let job_dir = Fpath.(datadir / job_name) in
|
let job_dir = Fpath.(datadir / job_name) in
|
||||||
let dest = Fpath.(job_dir / Uuidm.to_string uuid) in
|
let dest = Fpath.(job_dir / Uuidm.to_string uuid) in
|
||||||
Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ ->
|
Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ ->
|
||||||
|
@ -324,6 +328,25 @@ let prepare_staging staging_dir =
|
||||||
then Lwt_result.fail (`Msg "build directory already exists")
|
then Lwt_result.fail (`Msg "build directory already exists")
|
||||||
else Lwt_result.return ()
|
else Lwt_result.return ()
|
||||||
|
|
||||||
|
(* saving:
|
||||||
|
- for each artifact compute its sha256 checksum -- calling Lwt.pause in
|
||||||
|
between
|
||||||
|
- lookup artifact sha256 in the database and filter them out of the list: not_in_db
|
||||||
|
- mkdir -p _staging/uuid/
|
||||||
|
- save console & script to _staging/uuid/
|
||||||
|
- save each artifact in not_in_db as _staging/uuid/sha256
|
||||||
|
committing:
|
||||||
|
- for each artifact mv _staging/uuid/sha256 _artifacts/sha256
|
||||||
|
(or _artifacts/prefix(sha256)/sha256 where prefix(sha256) is the first two hex digits in sha256)
|
||||||
|
- now _staging/uuid only contains console & script so we mv _staging/uuid _staging/job/uuid
|
||||||
|
potential issues:
|
||||||
|
- race condition in uploading same artifact:
|
||||||
|
* if the artifact already exists in the database and thus filesystem then nothing is done
|
||||||
|
* if the artifact is added to the database and/or filesystem we atomically overwrite it
|
||||||
|
- input_id depends on a sort order?
|
||||||
|
*)
|
||||||
|
|
||||||
|
|
||||||
let add_build
|
let add_build
|
||||||
~datadir
|
~datadir
|
||||||
~cachedir
|
~cachedir
|
||||||
|
@ -344,16 +367,35 @@ let add_build
|
||||||
e)
|
e)
|
||||||
x
|
x
|
||||||
in
|
in
|
||||||
let artifacts_to_preserve =
|
|
||||||
let not_interesting p =
|
let not_interesting p =
|
||||||
String.equal (Fpath.basename p) "README.md" || String.equal (Fpath.get_ext p) ".build-hashes"
|
String.equal (Fpath.basename p) "README.md" || String.equal (Fpath.get_ext p) ".build-hashes"
|
||||||
in
|
in
|
||||||
List.filter (fun (p, _) -> not (not_interesting p)) raw_artifacts
|
begin
|
||||||
in
|
List.fold_left
|
||||||
|
(fun r (filepath, data) ->
|
||||||
|
r >>= fun acc ->
|
||||||
|
if not_interesting filepath then
|
||||||
|
Lwt_result.return acc
|
||||||
|
else
|
||||||
|
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data)
|
||||||
|
and size = String.length data in
|
||||||
|
Lwt_result.ok (Lwt.pause ()) >|= fun () ->
|
||||||
|
({ filepath; sha256; size }, data) :: acc)
|
||||||
|
(Lwt_result.return [])
|
||||||
|
raw_artifacts
|
||||||
|
end >>= fun artifacts ->
|
||||||
or_cleanup (prepare_staging staging_dir) >>= fun () ->
|
or_cleanup (prepare_staging staging_dir) >>= fun () ->
|
||||||
or_cleanup (save_console_and_script staging_dir job_name uuid console job.Builder.script)
|
or_cleanup (save_console_and_script staging_dir job_name uuid console job.Builder.script)
|
||||||
>>= fun (console, script) ->
|
>>= fun (console, script) ->
|
||||||
or_cleanup (save_all staging_dir job uuid artifacts_to_preserve) >>= fun artifacts ->
|
List.fold_left
|
||||||
|
(fun r ((f, _) as artifact) ->
|
||||||
|
r >>= fun acc ->
|
||||||
|
Db.find Builder_db.Build_artifact.exists f.sha256 >|= fun exists ->
|
||||||
|
if exists then acc else artifact :: acc)
|
||||||
|
(Lwt_result.return [])
|
||||||
|
artifacts >>= fun artifacts_to_save ->
|
||||||
|
or_cleanup (save_artifacts staging_dir artifacts_to_save) >>= fun () ->
|
||||||
|
let artifacts = List.map fst artifacts in
|
||||||
let r =
|
let r =
|
||||||
Db.start () >>= fun () ->
|
Db.start () >>= fun () ->
|
||||||
Db.exec Job.try_add job_name >>= fun () ->
|
Db.exec Job.try_add job_name >>= fun () ->
|
||||||
|
@ -422,7 +464,7 @@ let add_build
|
||||||
(Lwt_result.return ())
|
(Lwt_result.return ())
|
||||||
remaining_artifacts_to_add >>= fun () ->
|
remaining_artifacts_to_add >>= fun () ->
|
||||||
Db.commit () >>= fun () ->
|
Db.commit () >>= fun () ->
|
||||||
commit_files datadir staging_dir job_name uuid >|= fun () ->
|
commit_files datadir staging_dir job_name uuid artifacts >|= fun () ->
|
||||||
main_binary
|
main_binary
|
||||||
in
|
in
|
||||||
Lwt_result.bind_lwt_error (or_cleanup r)
|
Lwt_result.bind_lwt_error (or_cleanup r)
|
||||||
|
@ -451,7 +493,8 @@ let add_build
|
||||||
"--uuid=" ^ uuid ; "--platform=" ^ platform ;
|
"--uuid=" ^ uuid ; "--platform=" ^ platform ;
|
||||||
"--cache-dir=" ^ Fpath.to_string cachedir ;
|
"--cache-dir=" ^ Fpath.to_string cachedir ;
|
||||||
"--data-dir=" ^ Fpath.to_string datadir ;
|
"--data-dir=" ^ Fpath.to_string datadir ;
|
||||||
fp_str main_binary.localpath ])
|
"--main-binary-filepath=" ^ Fpath.to_string main_binary.filepath ;
|
||||||
|
fp_str Fpath.(datadir // artifact_path main_binary) ])
|
||||||
in
|
in
|
||||||
Log.debug (fun m -> m "executing hooks with %s" args);
|
Log.debug (fun m -> m "executing hooks with %s" args);
|
||||||
let dir = Fpath.(configdir / "upload-hooks") in
|
let dir = Fpath.(configdir / "upload-hooks") in
|
||||||
|
|
|
@ -5,6 +5,7 @@ val pp_error : Format.formatter -> error -> unit
|
||||||
val not_found : 'a option -> ('a, [> `Not_found ]) result Lwt.t
|
val not_found : 'a option -> ('a, [> `Not_found ]) result Lwt.t
|
||||||
|
|
||||||
val staging : Fpath.t -> Fpath.t
|
val staging : Fpath.t -> Fpath.t
|
||||||
|
val artifact_path : Builder_db.file -> Fpath.t
|
||||||
|
|
||||||
val cleanup_staging : Fpath.t -> Caqti_lwt.connection ->
|
val cleanup_staging : Fpath.t -> Caqti_lwt.connection ->
|
||||||
(unit, [> `Msg of string ]) result Lwt.t
|
(unit, [> `Msg of string ]) result Lwt.t
|
||||||
|
|
|
@ -188,7 +188,7 @@ let artifact
|
||||||
~basename
|
~basename
|
||||||
~job_name
|
~job_name
|
||||||
~build
|
~build
|
||||||
~file:{ Builder_db.filepath; localpath = _; sha256; size }
|
~file:{ Builder_db.filepath; sha256; size }
|
||||||
=
|
=
|
||||||
let artifact_link =
|
let artifact_link =
|
||||||
Link.Job_build_artifact.make
|
Link.Job_build_artifact.make
|
||||||
|
|
|
@ -32,6 +32,8 @@ Options:
|
||||||
Hex encoded SHA256 digest of the main binary.
|
Hex encoded SHA256 digest of the main binary.
|
||||||
--job=STRING
|
--job=STRING
|
||||||
Job name that was built.
|
Job name that was built.
|
||||||
|
--main-binary-filepath=STRING
|
||||||
|
The file path of the main binary.
|
||||||
EOM
|
EOM
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
@ -39,6 +41,7 @@ EOM
|
||||||
BUILD_TIME=
|
BUILD_TIME=
|
||||||
SHA=
|
SHA=
|
||||||
JOB=
|
JOB=
|
||||||
|
FILEPATH=
|
||||||
|
|
||||||
while [ $# -gt 1 ]; do
|
while [ $# -gt 1 ]; do
|
||||||
OPT="$1"
|
OPT="$1"
|
||||||
|
@ -53,6 +56,9 @@ while [ $# -gt 1 ]; do
|
||||||
--job=*)
|
--job=*)
|
||||||
JOB="${OPT##*=}"
|
JOB="${OPT##*=}"
|
||||||
;;
|
;;
|
||||||
|
--main-binary-filepath=*)
|
||||||
|
FILEPATH="${OPT##*=}"
|
||||||
|
;;
|
||||||
--*)
|
--*)
|
||||||
warn "Ignoring unknown option: '${OPT}'"
|
warn "Ignoring unknown option: '${OPT}'"
|
||||||
;;
|
;;
|
||||||
|
@ -67,13 +73,14 @@ done
|
||||||
[ -z "${BUILD_TIME}" ] && die "The --build-time option must be specified"
|
[ -z "${BUILD_TIME}" ] && die "The --build-time option must be specified"
|
||||||
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
|
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
|
||||||
[ -z "${JOB}" ] && die "The --job option must be specified"
|
[ -z "${JOB}" ] && die "The --job option must be specified"
|
||||||
|
[ -z "${FILEPATH}" ] && die "The --main-binary-filepath option must be specified"
|
||||||
|
|
||||||
FILENAME="${1}"
|
FILENAME="${1}"
|
||||||
|
|
||||||
: "${REPO:="/usr/local/www/pkg"}"
|
: "${REPO:="/usr/local/www/pkg"}"
|
||||||
: "${REPO_KEY:="/usr/local/etc/builder-web/repo.key"}"
|
: "${REPO_KEY:="/usr/local/etc/builder-web/repo.key"}"
|
||||||
|
|
||||||
if [ "$(basename "${FILENAME}" .pkg)" = "$(basename "${FILENAME}")" ]; then
|
if [ "$(basename "${FILEPATH}" .pkg)" = "$(basename "${FILEPATH}")" ]; then
|
||||||
echo "Not a FreeBSD package"
|
echo "Not a FreeBSD package"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -36,6 +36,8 @@ Options:
|
||||||
Job name that was built.
|
Job name that was built.
|
||||||
--platform=STRING
|
--platform=STRING
|
||||||
Platform name on which the build was performed.
|
Platform name on which the build was performed.
|
||||||
|
--main-binary-filepath=STRING
|
||||||
|
The file path of the main binary.
|
||||||
EOM
|
EOM
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
@ -44,6 +46,7 @@ BUILD_TIME=
|
||||||
SHA=
|
SHA=
|
||||||
JOB=
|
JOB=
|
||||||
PLATFORM=
|
PLATFORM=
|
||||||
|
FILEPATH=
|
||||||
|
|
||||||
while [ $# -gt 1 ]; do
|
while [ $# -gt 1 ]; do
|
||||||
OPT="$1"
|
OPT="$1"
|
||||||
|
@ -61,6 +64,9 @@ while [ $# -gt 1 ]; do
|
||||||
--platform=*)
|
--platform=*)
|
||||||
PLATFORM="${OPT##*=}"
|
PLATFORM="${OPT##*=}"
|
||||||
;;
|
;;
|
||||||
|
--main-binary-filepath=*)
|
||||||
|
FILEPATH="${OPT##*=}"
|
||||||
|
;;
|
||||||
--*)
|
--*)
|
||||||
warn "Ignoring unknown option: '${OPT}'"
|
warn "Ignoring unknown option: '${OPT}'"
|
||||||
;;
|
;;
|
||||||
|
@ -76,10 +82,11 @@ done
|
||||||
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
|
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
|
||||||
[ -z "${JOB}" ] && die "The --job option must be specified"
|
[ -z "${JOB}" ] && die "The --job option must be specified"
|
||||||
[ -z "${PLATFORM}" ] && die "The --platform option must be specified"
|
[ -z "${PLATFORM}" ] && die "The --platform option must be specified"
|
||||||
|
[ -z "${FILEPATH}" ] && die "The --main-binary-filepath option must be specified"
|
||||||
|
|
||||||
FILENAME="${1}"
|
FILENAME="${1}"
|
||||||
|
|
||||||
if [ $(basename "${FILENAME}" .deb) = $(basename "${FILENAME}") ]; then
|
if [ $(basename "${FILEPATH}" .deb) = $(basename "${FILEPATH}") ]; then
|
||||||
echo "Not a Debian package"
|
echo "Not a Debian package"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -75,7 +75,8 @@ info "processing UUID '${UUID}'"
|
||||||
DB="${DATA_DIR}/builder.sqlite3"
|
DB="${DATA_DIR}/builder.sqlite3"
|
||||||
|
|
||||||
get_main_binary () {
|
get_main_binary () {
|
||||||
sqlite3 "${DB}" "SELECT ba.localpath FROM build AS b
|
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
|
||||||
|
FROM build AS b
|
||||||
JOIN build_artifact AS ba ON ba.build = b.id AND b.main_binary = ba.id
|
JOIN build_artifact AS ba ON ba.build = b.id AND b.main_binary = ba.id
|
||||||
WHERE uuid = '${UUID}';"
|
WHERE uuid = '${UUID}';"
|
||||||
}
|
}
|
||||||
|
@ -84,17 +85,19 @@ BIN="${DATA_DIR}/$(get_main_binary)" || die "Failed to get main binary from data
|
||||||
[ -z "${BIN}" ] && die "No main-binary found in db '${DB}' for build '${UUID}'"
|
[ -z "${BIN}" ] && die "No main-binary found in db '${DB}' for build '${UUID}'"
|
||||||
|
|
||||||
get_debug_binary () {
|
get_debug_binary () {
|
||||||
sqlite3 "${DB}" "SELECT ba.localpath FROM build AS b
|
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
|
||||||
|
FROM build AS b
|
||||||
JOIN build_artifact AS ba ON ba.build = b.id
|
JOIN build_artifact AS ba ON ba.build = b.id
|
||||||
WHERE
|
WHERE
|
||||||
uuid = '${UUID}'
|
uuid = '${UUID}'
|
||||||
AND ba.localpath LIKE '%.debug';"
|
AND ba.filepath LIKE '%.debug';"
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_BIN_RELATIVE="$(get_debug_binary)" || die "Failed to get debug binary from database"
|
DEBUG_BIN_RELATIVE="$(get_debug_binary)" || die "Failed to get debug binary from database"
|
||||||
|
|
||||||
get_opam_switch () {
|
get_opam_switch () {
|
||||||
sqlite3 "${DB}" "SELECT ba.localpath FROM build AS b
|
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
|
||||||
|
FROM build AS b
|
||||||
JOIN build_artifact AS ba ON ba.build = b.id
|
JOIN build_artifact AS ba ON ba.build = b.id
|
||||||
WHERE
|
WHERE
|
||||||
uuid = '${UUID}'
|
uuid = '${UUID}'
|
||||||
|
|
|
@ -43,18 +43,15 @@ module Testable = struct
|
||||||
let file =
|
let file =
|
||||||
let equal (x : Builder_db.Rep.file) (y : Builder_db.Rep.file) =
|
let equal (x : Builder_db.Rep.file) (y : Builder_db.Rep.file) =
|
||||||
Fpath.equal x.filepath y.filepath &&
|
Fpath.equal x.filepath y.filepath &&
|
||||||
Fpath.equal x.localpath y.localpath &&
|
|
||||||
Cstruct.equal x.sha256 y.sha256 &&
|
Cstruct.equal x.sha256 y.sha256 &&
|
||||||
x.size = y.size
|
x.size = y.size
|
||||||
in
|
in
|
||||||
let pp ppf { Builder_db.Rep.filepath; localpath; sha256; size } =
|
let pp ppf { Builder_db.Rep.filepath; sha256; size } =
|
||||||
Format.fprintf ppf "{@[<v 1>@;<1 0>Builder_db.Rep.filepath = %a;@;<1 0>\
|
Format.fprintf ppf "{@[<v 1>@;<1 0>Builder_db.Rep.filepath = %a;@;<1 0>\
|
||||||
localpath = %a;@;<1 0>\
|
|
||||||
sha256 = %a;@;<1 0>\
|
sha256 = %a;@;<1 0>\
|
||||||
size = %d;@;<1 0>\
|
size = %d;@;<1 0>\
|
||||||
@]@,}"
|
@]@,}"
|
||||||
Fpath.pp filepath Fpath.pp localpath
|
Fpath.pp filepath Cstruct.hexdump_pp sha256 size
|
||||||
Cstruct.hexdump_pp sha256 size
|
|
||||||
in
|
in
|
||||||
Alcotest.testable pp equal
|
Alcotest.testable pp equal
|
||||||
|
|
||||||
|
@ -133,11 +130,10 @@ let finish = Option.get (Ptime.of_float_s 1.)
|
||||||
let result = Builder.Exited 0
|
let result = Builder.Exited 0
|
||||||
let main_binary =
|
let main_binary =
|
||||||
let filepath = Result.get_ok (Fpath.of_string "bin/hello.sh") in
|
let filepath = Result.get_ok (Fpath.of_string "bin/hello.sh") in
|
||||||
let localpath = Result.get_ok (Fpath.of_string "/dev/null") in
|
|
||||||
let data = "#!/bin/sh\necho Hello, World\n" in
|
let data = "#!/bin/sh\necho Hello, World\n" in
|
||||||
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
||||||
let size = String.length data in
|
let size = String.length data in
|
||||||
{ Builder_db.Rep.filepath; localpath; sha256; size }
|
{ Builder_db.Rep.filepath; sha256; size }
|
||||||
let main_binary2 =
|
let main_binary2 =
|
||||||
let data = "#!/bin/sh\necho Hello, World 2\n" in
|
let data = "#!/bin/sh\necho Hello, World 2\n" in
|
||||||
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
|
||||||
|
@ -149,7 +145,6 @@ let fail_if_none a =
|
||||||
Option.to_result ~none:(`Msg "Failed to retrieve") a
|
Option.to_result ~none:(`Msg "Failed to retrieve") a
|
||||||
|
|
||||||
let add_test_build user_id (module Db : CONN) =
|
let add_test_build user_id (module Db : CONN) =
|
||||||
let r =
|
|
||||||
let open Builder_db in
|
let open Builder_db in
|
||||||
Db.start () >>= fun () ->
|
Db.start () >>= fun () ->
|
||||||
Db.exec Job.try_add job_name >>= fun () ->
|
Db.exec Job.try_add job_name >>= fun () ->
|
||||||
|
@ -161,9 +156,6 @@ let add_test_build user_id (module Db : CONN) =
|
||||||
Db.find last_insert_rowid () >>= fun main_binary_id ->
|
Db.find last_insert_rowid () >>= fun main_binary_id ->
|
||||||
Db.exec Build.set_main_binary (id, main_binary_id) >>= fun () ->
|
Db.exec Build.set_main_binary (id, main_binary_id) >>= fun () ->
|
||||||
Db.commit ()
|
Db.commit ()
|
||||||
in
|
|
||||||
Result.fold ~ok:Result.ok ~error:(fun _ -> Db.rollback ())
|
|
||||||
r
|
|
||||||
|
|
||||||
let with_build_db f () =
|
let with_build_db f () =
|
||||||
or_fail
|
or_fail
|
||||||
|
@ -269,6 +261,14 @@ let test_artifact_get_by_build_uuid (module Db : CONN) =
|
||||||
get_opt "no build" >>| fun (_id, file) ->
|
get_opt "no build" >>| fun (_id, file) ->
|
||||||
Alcotest.(check Testable.file) "same file" file main_binary
|
Alcotest.(check Testable.file) "same file" file main_binary
|
||||||
|
|
||||||
|
let test_artifact_exists_true (module Db : CONN) =
|
||||||
|
Db.find Builder_db.Build_artifact.exists main_binary.sha256 >>| fun exists ->
|
||||||
|
Alcotest.(check bool) "main binary exists" true exists
|
||||||
|
|
||||||
|
let test_artifact_exists_false (module Db : CONN) =
|
||||||
|
Db.find Builder_db.Build_artifact.exists main_binary2.sha256 >>| fun exists ->
|
||||||
|
Alcotest.(check bool) "main binary2 doesn't exists" false exists
|
||||||
|
|
||||||
(* XXX: This test should fail because main_binary on the corresponding build
|
(* XXX: This test should fail because main_binary on the corresponding build
|
||||||
* references its main_binary. This is not the case now due to foreign key. *)
|
* references its main_binary. This is not the case now due to foreign key. *)
|
||||||
let test_artifact_remove_by_build (module Db : CONN) =
|
let test_artifact_remove_by_build (module Db : CONN) =
|
||||||
|
@ -306,6 +306,8 @@ let () =
|
||||||
"build-artifact", [
|
"build-artifact", [
|
||||||
test_case "Get all by build" `Quick (with_build_db test_artifact_get_all_by_build);
|
test_case "Get all by build" `Quick (with_build_db test_artifact_get_all_by_build);
|
||||||
test_case "Get by build uuid" `Quick (with_build_db test_artifact_get_by_build_uuid);
|
test_case "Get by build uuid" `Quick (with_build_db test_artifact_get_by_build_uuid);
|
||||||
|
test_case "Artifact exists" `Quick (with_build_db test_artifact_exists_true);
|
||||||
|
test_case "Other artifact doesn't exists" `Quick (with_build_db test_artifact_exists_false);
|
||||||
test_case "Remove by build" `Quick (with_build_db test_artifact_remove_by_build);
|
test_case "Remove by build" `Quick (with_build_db test_artifact_remove_by_build);
|
||||||
];
|
];
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue