Redesign of Builder web #18

Open
PixieDust wants to merge 16 commits from builder_web_design into main
3 changed files with 1196 additions and 277 deletions

848
lib/styles.ml Normal file
View file

@ -0,0 +1,848 @@
let static_css = Tyxml.Html.Unsafe.data {|
*,
:after,
:before {
box-sizing: border-box;
border: 0 solid #e5e7eb
}
:after,
:before {
--tw-content: ""
}
:host,
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-feature-settings: normal;
font-variation-settings: normal;
-webkit-tap-highlight-color: transparent
}
.dark\:bg-black-molly {
&:where(.dark, .dark *) {
--tw-bg-opacity: 1;
background-color: #0B0D12 !important;
color: #FFFFFF !important
}
}
.dark\:text-gray-50 {
&:where(.dark, .dark *) {
color: #FFFFFF !important
}
}
#robur-logo {
transform: translate(-20%, 100%) rotate(270deg);
width: 60em;
position: fixed;
}
@media (max-width: 767px) {
#robur-logo {
display: none !important; /* Hide on small screens */
}
}
nav ul {
display: flex;
list-style: none;
}
nav ul li::before {
content: "";
}
nav ul li:first-child::before {
content: "";
}
nav a {
padding: .5em 1em;
}
body {
margin: 0;
line-height: inherit
}
hr {
height: 0;
color: inherit;
border-top-width: 1px
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
margin-top: 1rem;
margin-bottom: 1rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
}
a {
color: inherit;
text-decoration: inherit
}
b,
strong {
font-weight: bolder
}
code,
kbd,
pre,
samp {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
font-feature-settings: normal;
font-variation-settings: normal;
font-size: 1em
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
table-layout: auto;
min-width: 100%;
margin-top: 1rem;
margin-bottom: 1rem
}
th {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
padding-top: 1rem;
padding-bottom: 1rem;
text-align: center;
font-weight: 700;
--tw-text-opacity: 1;
color: rgb(54 156 140/var(--tw-text-opacity));
text-transform: uppercase;
}
td {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
padding-top: 1rem;
padding-bottom: 1rem;
font-weight: 500;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
font-size: 100%;
font-weight: inherit;
line-height: inherit;
letter-spacing: inherit;
color: inherit;
margin: 0;
padding: 0
}
button,
select {
text-transform: none
}
button,
input:where([type=button]),
input:where([type=reset]),
input:where([type=submit]) {
-webkit-appearance: button;
background-color: initial;
background-image: none
}
blockquote,
dd,
dl,
figure,
hr,
p,
pre {
margin: 0
}
fieldset {
margin: 0
}
fieldset,
legend {
padding: 0
}
menu,
ol,
ul {
list-style: none;
margin: 0;
padding: 0
}
dialog {
padding: 0
}
textarea {
resize: vertical
}
[role=button],
button {
cursor: pointer
}
:disabled {
cursor: default
}
audio,
canvas,
embed,
iframe,
img,
object,
svg,
video {
display: block;
vertical-align: middle
}
img,
video {
max-width: 100%;
height: auto
}
[hidden] {
display: none
}
.container {
width: 100%
}
.wrap {
text-wrap: wrap;
word-break: normal;
overflow-wrap: break-word;
}
@media (min-width:640px) {
.container {
max-width: 640px
}
}
@media (min-width:768px) {
.container {
max-width: 768px
}
}
@media (min-width:1024px) {
.container {
max-width: 1024px
}
}
@media (min-width:1280px) {
.container {
max-width: 1280px
}
}
@media (min-width:1536px) {
.container {
max-width: 1536px
}
}
.link {
--tw-text-opacity: 1;
color: rgb(54 156 140/var(--tw-text-opacity));
text-decoration: underline;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
margin-top: .5rem;
margin-bottom: .5rem
font-size: 1rem;
line-height: 1.5rem
}
.link-red {
--tw-text-opacity: 1;
color: rgb(255 78 51/var(--tw-text-opacity));
text-decoration: underline;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
margin-top: .5rem;
margin-bottom: .5rem
font-size: 1rem;
line-height: 1.5rem
}
.col-span-1 {
grid-column: span 1/span 1
}
.col-span-3 {
grid-column: span 3/span 3
}
.m-auto {
margin: auto
}
.mx-auto {
margin-left: auto;
margin-right: auto
}
.my-2 {
margin-top: .5rem;
margin-bottom: .5rem
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem
}
.block {
display: block
}
.inline-block {
display: inline-block
}
.flex {
display: flex
}
.inline-flex {
display: inline-flex
}
.grid {
display: grid
}
.hidden {
display: none
}
.w-full {
width: 100%
}
.min-w-full {
min-width: 100%
}
.flex-none {
flex: none
}
.border-collapse {
border-collapse: collapse
}
.cursor-pointer {
cursor: pointer
}
.list-disc {
list-style-type: disc;
}
.list-inside {
list-style-position: inside;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr))
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr))
}
.flex-col {
flex-direction: column
}
.items-start {
align-items: flex-start
}
.items-end {
align-items: flex-end
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.justify-items-center {
justify-items: center
}
.gap-4 {
gap: 1rem
}
.gap-8 {
gap: 2rem
}
.space-x-2>:not([hidden])~:not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(.5rem*var(--tw-space-x-reverse));
margin-left: calc(.5rem*(1 - var(--tw-space-x-reverse)))
}
.space-x-4>:not([hidden])~:not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem*var(--tw-space-x-reverse));
margin-left: calc(1rem*(1 - var(--tw-space-x-reverse)))
}
.space-y-2>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(.5rem*(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(.5rem*var(--tw-space-y-reverse))
}
.divide-y>:not([hidden])~:not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px*(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px*var(--tw-divide-y-reverse))
}
.divide-gray-200>:not([hidden])~:not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235/var(--tw-divide-opacity))
}
.divide-gray-600>:not([hidden])~:not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgba(75, 85, 99, var(--tw-divide-opacity));
}
.rounded {
border-radius: .25rem
}
.rounded-2xl {
border-radius: 1rem
}
.rounded-3xl {
border-radius: 1.5rem
}
.rounded-full {
border-radius: 9999px
}
.rounded-lg {
border-radius: .5rem
}
.rounded-md {
border-radius: .375rem
}
.rounded-xl {
border-radius: .75rem
}
.border {
border-width: 1px
}
.border-t-1 {
border-top-width: 1px
}
.bg-black-molly {
--tw-bg-opacity: 1;
background-color: #0B0D12;
color: #FFFFFF
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246/var(--tw-bg-opacity))
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251/var(--tw-bg-opacity))
}
.bg-gray-500\/25 {
background-color: #6b728040
}
.bg-primary-500 {
--tw-bg-opacity: 1;
background-color: rgb(54 156 140/var(--tw-bg-opacity))
}
.bg-primary-800 {
--tw-bg-opacity: 1;
background-color: rgb(32 79 74/var(--tw-bg-opacity))
}
.bg-secondary-500 {
--tw-bg-opacity: 1;
background-color: rgb(255 78 51/var(--tw-bg-opacity))
}
.bg-secondary-700 {
--tw-bg-opacity: 1;
background-color: rgb(200 38 13/var(--tw-bg-opacity))
}
.p-10 {
padding: 2.5rem
}
.p-2 {
padding: .5rem
}
.p-4 {
padding: 1rem
}
.px-2 {
padding-left: .5rem;
padding-right: .5rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-top: .5rem;
padding-bottom: .5rem
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem
}
.text-center {
text-align: center
}
.text-right {
text-align: right
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
.text-7xl {
font-size: 4.5rem;
line-height: 1
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.font-semibold {
font-weight: 600
}
.uppercase {
text-transform: uppercase
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219/var(--tw-text-opacity))
}
.text-gray-50 {
--tw-text-opacity: 1;
color: rgb(249 250 251/var(--tw-text-opacity))
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55/var(--tw-text-opacity))
}
.text-primary-500 {
--tw-text-opacity: 1;
color: rgb(54 156 140/var(--tw-text-opacity))
}
.text-primary-800 {
--tw-text-opacity: 1;
color: rgb(32 79 74/var(--tw-text-opacity))
}
.text-secondary-500 {
--tw-text-opacity: 1;
color: rgb(255 78 51/var(--tw-text-opacity))
}
.text-secondary-700 {
--tw-text-opacity: 1;
color: rgb(200 38 13/var(--tw-text-opacity))
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color)
}
.outline-0 {
outline-width: 0
}
.ring-primary-200 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(171 228 214/var(--tw-ring-opacity))
}
.hover\:border-primary-200:hover {
--tw-border-opacity: 1;
border-color: rgb(171 228 214/var(--tw-border-opacity))
}
.hover\:border-transparent:hover {
border-color: #0000
}
.hover\:bg-gray-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235/var(--tw-bg-opacity))
}
.hover\:bg-primary-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(213 242 235/var(--tw-bg-opacity))
}
.hover\:bg-primary-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(35 98 90/var(--tw-bg-opacity))
}
.hover\:bg-primary-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(32 79 74/var(--tw-bg-opacity))
}
.hover\:bg-secondary-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 227 223/var(--tw-bg-opacity))
}
.hover\:bg-secondary-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(200 38 13/var(--tw-bg-opacity))
}
.hover\:bg-secondary-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(165 35 15/var(--tw-bg-opacity))
}
.hover\:bg-opacity-50:hover {
--tw-bg-opacity: 0.5
}
.hover\:font-bold:hover {
font-weight: 700
}
.hover\:text-gray-50:hover {
--tw-text-opacity: 1;
color: rgb(249 250 251/var(--tw-text-opacity))
}
.hover\:text-primary-400:hover {
--tw-text-opacity: 1;
color: rgb(78 179 161/var(--tw-text-opacity))
}
.hover\:text-primary-50:hover {
--tw-text-opacity: 1;
color: rgb(243 250 249/var(--tw-text-opacity))
}
.hover\:text-primary-500:hover {
--tw-text-opacity: 1;
color: rgb(54 156 140/var(--tw-text-opacity))
}
.hover\:text-primary-700:hover {
--tw-text-opacity: 1;
color: rgb(35 98 90/var(--tw-text-opacity))
}
.hover\:text-primary-800:hover {
--tw-text-opacity: 1;
color: rgb(32 79 74/var(--tw-text-opacity))
}
.hover\:text-secondary-800:hover {
--tw-text-opacity: 1;
color: rgb(165 35 15/var(--tw-text-opacity))
}
@media (prefers-reduced-motion:reduce) {
.motion-reduce\:transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
transition-duration: .15s
}
}
@media (min-width:768px) {
.md\:block {
display: block
}
.md\:flex {
display: flex
}
.md\:grid {
display: grid
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr))
}
.md\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr))
}
.md\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr))
}
.md\:col-span-1 {
grid-column: span 1/span 1
}
.md\:col-span-3 {
grid-column: span 3/span 3
}
.md\:text-7xl {
font-size: 4.5rem;
line-height: 1
}
}
@media (prefers-color-scheme:dark) {
.dark\:border-gray-700 {
--tw-border-opacity: 1;
border-color: rgb(55 65 81/var(--tw-border-opacity))
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39/var(--tw-bg-opacity))
}
.dark\:text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219/var(--tw-text-opacity))
}
.dark\:after\:bg-gray-300:after {
content: var(--tw-content);
--tw-bg-opacity: 1;
background-color: rgb(209 213 219/var(--tw-bg-opacity))
}
.peer:checked~.dark\:peer-checked\:bg-primary-500 {
--tw-bg-opacity: 1;
background-color: rgb(54 156 140/var(--tw-bg-opacity))
}
.peer:focus~.dark\:peer-focus\:outline-gray-300 {
outline-color: #d1d5db
}
.peer:focus:checked~.dark\:peer-focus\:peer-checked\:outline-primary-500 {
outline-color: #369c8c
}
}
|}

View file

@ -11,13 +11,13 @@ let check_icon result =
match result with match result with
| Builder.Exited 0 -> | Builder.Exited 0 ->
H.span ~a:H.[ H.span ~a:H.[
a_style "color: green; cursor: pointer;"; a_class ["text-primary-500 cursor-pointer"];
a_titlef "%a" Builder.pp_execution_result result; a_titlef "%a" Builder.pp_execution_result result;
] ]
[H.txt ""] [H.txt ""]
| _ -> | _ ->
H.span ~a:H.[ H.span ~a:H.[
a_style "color: red; cursor: pointer;"; a_class ["text-secondary-500 cursor-pointer"];
a_titlef "%a" Builder.pp_execution_result result; a_titlef "%a" Builder.pp_execution_result result;
] ]
[H.txt ""] [H.txt ""]
@ -32,52 +32,6 @@ type nav = [
let pp_platform = let pp_platform =
Fmt.(option ~none:(any "") (append (any "on ") string)) Fmt.(option ~none:(any "") (append (any "on ") string))
let static_css = Tyxml.Html.Unsafe.data {|
body {
margin: 40px auto;
line-height: 1.6;
color: #444;
background: rgb(200,200,200);
padding: 0 10px;
}
nav ul {
display: flex;
list-style: none;
}
nav ul li::before {
content: "";
}
nav ul li:first-child::before {
content: "";
}
nav a {
padding: .5em 1em;
}
h1,h2,h3{line-height:1.2}
.output-ts {
white-space: nowrap;
cursor: pointer;
user-select: none;
}
.output-ts a {text-decoration: none;}
.output-ts a:hover {text-decoration: underline;}
.output-code {
overflow: visible;
white-space: pre;
}
.toggleable {
display: none;
}
.toggleable-descr {
cursor: pointer;
text-decoration: underline;
user-select: none;
}
:checked + .toggleable {
display: block;
}
|}
let make_breadcrumbs nav = let make_breadcrumbs nav =
let to_nav kvs = let to_nav kvs =
H.nav [ H.nav [
@ -131,35 +85,95 @@ let make_breadcrumbs nav =
let layout let layout
?include_static_css ?include_static_css
?(nav=`Default) ?(nav=`Default)
?(manual_width=false)
~title ~title
body body
= =
let breadcrumb = make_breadcrumbs nav in let breadcrumb = make_breadcrumbs nav in
(*> Note: Last declared CSS wins - so one can override here*) (*> Note: Last declared CSS wins - so one can override here*)
let static_css = static_css :: Option.to_list include_static_css let static_css = Styles.static_css :: Option.to_list include_static_css
in
let body =
let style_grid_container = H.a_style "\
display: flex;
align-items: center;
justify-content: center;
min-width: 83em;
"
and style_grid = H.a_style @@
if manual_width then "" else "\
width: 76%;\
"
in
[ H.div ~a:[ style_grid_container ]
[ H.div ~a:[ style_grid ] body ]]
in in
H.html H.html
(H.head (H.title (H.txt title)) (H.head (H.title (H.txt title))
[H.style ~a:H.[a_mime_type "text/css"] static_css]) [
PixieDust marked this conversation as resolved Outdated

if you're not going to use these why keep them?

if you're not going to use these why keep them?

Yes I'll do some code cleaning to remove the things we aren't using

Yes I'll do some code cleaning to remove the things we aren't using
(H.body [ H.meta ~a:[ H.a_charset "UTF-8" ] ();
breadcrumb; H.meta ~a:[H.a_name "viewport"; H.a_content "width=device-width, initial-scale=1.0";]();
H.style ~a:H.[a_mime_type "text/css"] static_css])
(H.body ~a:[H.a_class ["bg-gray-50 dark:bg-black-molly w-full text-gray-800 dark:text-gray-50 mx-auto p-10 md:grid md:grid-cols-4"]] [
H.div ~a:[H.a_class ["text-center md:col-span-1 hidden md:block"]; H.a_style ""] [
H.img ~a:[H.a_id "robur-logo"] ~src:"https://i.ibb.co/Y4YsvcDb/robur-logo.png" ~alt:"Robur Logo" ()
];
H.div ~a:[H.a_class ["mx-auto w-full md:col-span-3 px-4"]] [
H.div ~a:[H.a_class ["md:flex justify-between items-center"]] [
H.div [breadcrumb];
H.div ~a:[H.a_class ["flex items-center space-x-4"]] [
H.form ~a:[H.a_action "/hash"; H.a_method `Get; H.a_class ["my-4 p-4"]] [
H.label ~a:[H.a_class ["block text-lg font-semibold my-2 text-right"]] [
H.txt "Search artifact by SHA256";
];
H.div ~a:[H.a_class ["w-full flex space-x-2 justify-end justify-items-center items-center"]] [
H.div [
H.input ~a:[
H.a_input_type `Search;
H.a_id "sha256";
H.a_required ();
H.a_name "sha256";
H.a_class ["w-full border bg-gray-200 text-gray-800 rounded px-3 py-2 focus:ring-0 focus:ring-primary-200"]
] ()
];
H.div ~a:[H.a_class ["text-center"]] [
H.input ~a:[
H.a_input_type `Submit;
H.a_value "Search";

Do we need this many css classes? I don't even know what they mean.

Do we need this many css classes? I don't even know what they mean.
H.a_class ["my-4 bg-primary-500 text-gray-50 cursor-pointer font-bold py-2 px-4 rounded hover:bg-primary-800"]
] ()
]
]
];
H.button ~a:[
H.a_id "theme-toggle";
H.a_class ["p-2 rounded-full border border-gray-300 bg-gray-100 dark:bg-gray-700 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600"];
] [
H.txt "🌙"
]
]
];
H.main body H.main body
];
H.script
(H.txt "
document.addEventListener('DOMContentLoaded', function () {
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
const logo = document.getElementById('robur-logo'); // Ensure the image has this ID
function updateTheme() {
const isDark = html.classList.contains('dark');
themeToggle.innerText = isDark ? '' : '🌑';
logo.src = isDark
? 'https://i.ibb.co/Y4YsvcDb/robur-logo.png' // Dark mode logo
: 'https://i.ibb.co/r2DRDdTt/robur-logo-black-writing.png'; // Light mode logo
}
function toggleTheme() {
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
updateTheme();
}
// Load user preference from localStorage
if (localStorage.getItem('theme') === 'dark') {

I think this can also be achieved with css.

I think this can also be achieved with css.
html.classList.add('dark');
}
// Set correct icon and logo on load
updateTheme();
// Attach event listener (in case onclick fails)
themeToggle.addEventListener('click', toggleTheme);
});
")
]) ])
let toggleable ?(hidden=true) ~id ~description content = let toggleable ?(hidden=true) ~id ~description content =
@ -196,14 +210,14 @@ let artifact
~build:build.Builder_db.Build.uuid ~build:build.Builder_db.Build.uuid
~artifact:(`File filepath) () ~artifact:(`File filepath) ()
in in
[ H.div [
H.a ~a:H.[a_href artifact_link] [ H.a ~a:[H.a_href artifact_link; H.a_class ["link"]] [
if basename then H.txt (Fpath.basename filepath) (if basename then H.txt ("Download " ^ Fpath.basename filepath)
else txtf "%a" Fpath.pp filepath else txtf "Download %a" Fpath.pp filepath);
];
H.txt " ";
H.code [txtf "SHA256:%s" (Ohex.encode sha256)];
txtf " (%a)" Fmt.byte_size size; txtf " (%a)" Fmt.byte_size size;
];
H.br ();
H.p ~a:[H.a_class ["wrap"]] [txtf "SHA256:%s" (Ohex.encode sha256)];
] ]
let page_not_found ~target ~referer = let page_not_found ~target ~referer =
@ -236,7 +250,7 @@ let viz_not_found =
[ txtf "%s" title ]; [ txtf "%s" title ];
] ]
in in
let static_css = static_css :: [ Tyxml.Html.Unsafe.data "\ let static_css = Styles.static_css :: [ Tyxml.Html.Unsafe.data "\
body { background: rgb(191,191,191); }\ body { background: rgb(191,191,191); }\
"] "]
in in
@ -251,120 +265,152 @@ let viz_not_found =
module Builds = struct module Builds = struct
let data =
{|
# Reproducible OPAM builds
This website offers binary MirageOS unikernels and supplementary OS packages.
If you want to use our binary packages and setup unikernels, follow
[these instructions](https://robur.coop/Projects/Reproducible_builds).
The unikernels are statically linked executables where the execution target is
independent of the build platform - so even if they're compiled on a FreeBSD
system they can be run on a Linux or OpenBSD host. Many are executed using a
[solo5](https://github.com/solo5/solo5) tender.
The filename suffix of the unikernel binary indicate the expected execution environment:
- `.hvt`: hardware virtualized - requires `solo5-hvt`
([Linux KVM](https://www.linux-kvm.org/page/Main_Page),
[FreeBSD BHyve](https://wiki.freebsd.org/bhyve),
[OpenBSD VMM](https://man.openbsd.org/vmm)),
- `.spt`: sandboxed process - requires `solo5-spt` (Linux with seccomp),
- `.xen`: Xen PVH virtual machine (on a Xen or QubesOS host),
- `.virtio`: any virtio environment (qemu, GCE, KVM, BHyve),
- `.muen`: on [muen](https://muen.sk).
A persistent link to the latest successful build is available as
`/job/*jobname*/build/latest/`. Each build can be reproduced with
[orb](https://github.com/robur-coop/orb/). The builds are scheduled and executed
daily by [builder](https://github.com/robur-coop/builder/). This web interface is
[builder-web](https://git.robur.coop/robur/builder-web/). Read further information
[on our project page](https://robur.coop/Projects/Reproducible_builds). This
work has been funded by the European Union under the
[NGI Pointer](https://pointer.ngi.eu) program. Contact team ATrobur.coop if you
have questions or suggestions.
|}
let make_header = let make_header =
[ H.[
H.Unsafe.data (Utils.md_to_html data); div ~a:[a_class ["header container mx-auto px-4 py-8 text-gray-800 w-full"]] [

Why not keep the markdown?

Why not keep the markdown?

This is so that I can style the text in different ways. I use a grid to do this

This is so that I can style the text in different ways. I use a grid to do this
H.form ~a:H.[a_action "/hash"; a_method `Get] [ (* Logo Section *)
H.label [ div ~a:[a_class ["flex items-center my-4"]] [
H.txt "Search artifact by SHA256";
H.br ();
H.input ~a:H.[
a_input_type `Search;
a_id "sha256";
a_name "sha256";
] ();
]; ];
H.input ~a:H.[ h1 ~a:[a_class ["md:text-7xl text-4xl font-bold text-primary-500 text-center"]]
a_input_type `Submit; [txt "Reproducible OPAM Builds"]
a_value "Search"; ];
] ();
div ~a:[a_class ["md:grid grid-cols-2 gap-4"]] [
div [
p ~a:[a_class ["text-lg my-4"]] [
txt "This website offers binary MirageOS unikernels and supplementary OS packages. ";
txt "If you want to use our binary packages and setup unikernels, follow ";
a ~a:[a_href "https://robur.coop/Projects/Reproducible_builds"; a_class ["link"]]
[txt "these instructions"];
txt "."
];
p ~a:[a_class ["text-lg my-4"]] [
txt "The unikernels are statically linked executables where the execution target is ";
txt "independent of the build platform - so even if they're compiled on a FreeBSD ";
txt "system they can be run on a Linux or OpenBSD host. Many are executed using a ";
a ~a:[a_href "https://github.com/solo5/solo5"; a_class ["link"]]
[txt "solo5"];
txt " tender."
];
];
div [
p ~a:[a_class ["text-lg my-4"]] [
txt "A persistent link to the latest successful build is available as ";
code ~a:[a_class ["px-2 py-1 rounded text-sm text-primary-500"]]
[txt "/job/*jobname*/build/latest/"];
txt ". Each build can be reproduced with ";
a ~a:[a_href "https://github.com/robur-coop/orb/"; a_class ["link"]]
[txt "orb"];
txt "."
];
p ~a:[a_class ["text-lg my-4"]] [
txt "The builds are scheduled and executed daily by ";
a ~a:[a_href "https://github.com/robur-coop/builder/"; a_class ["link"]]
[txt "builder"];
txt ". This web interface is ";
a ~a:[a_href "https://git.robur.coop/robur/builder-web/"; a_class ["link"]]
[txt "builder-web"];
txt ". Read further information ";
a ~a:[a_href "https://robur.coop/Projects/Reproducible_builds"; a_class ["link"]]
[txt "on our project page"];
txt "."
];
p ~a:[a_class ["text-lg my-4"]] [
txt "This work has been funded by the European Union under the ";
a ~a:[a_href "https://pointer.ngi.eu"; a_class ["link"]]
[txt "NGI Pointer"];
txt " program. Contact team AT robur.coop if you have questions or suggestions."
];
];
];
div ~a:[a_class ["my-4"]] [
h2
[txt "Execution Environments"];
ul ~a:[a_class ["list-disc list-inside text-lg space-y-2"]] [
li [span ~a:[a_class ["text-primary-500"]] [txt ".spt: "]; txt "sandboxed process - requires solo5-spt (Linux with seccomp)"];
li [span ~a:[a_class ["text-primary-500"]] [txt ".xen: "]; txt "Xen PVH virtual machine (on a Xen or QubesOS host)"];
li [span ~a:[a_class ["text-primary-500"]] [txt ".virtio: "]; txt "any virtio environment (qemu, GCE, KVM, BHyve)"];
li [span ~a:[a_class ["text-primary-500"]] [txt ".muem: "]; txt "on muen"]
]; ];
] ]
]
let make_platform_builds ~job_name (platform, latest_build, latest_artifact) = let make_platform_builds ~job_name (platform, latest_build, latest_artifact) =
H.[
div ~a:[a_class ["md:grid grid-cols-3 space-y-2 p-2 rounded-lg"]]
[
div ~a:[a_class ["flex items-center space-x-2"]]
[ [
check_icon latest_build.Builder_db.Build.result; check_icon latest_build.Builder_db.Build.result;
H.txt " "; a ~a:[
H.a ~a:[ a_href @@ Link.Job.make ~job_name ~queries:[ `Platform platform ] ();
H.a_href @@ Link.Job.make ~job_name a_class ["link font-medium"]
~queries:[ `Platform platform ] ()
] ]
[H.txt platform]; [txt platform]
H.txt " "; ];
H.a ~a:[ div ~a:[a_class ["text-gray-300"]]
H.a_href @@ Link.Job_build.make [ a ~a:[
a_href @@ Link.Job_build.make
~job_name ~job_name
~build:latest_build.Builder_db.Build.uuid ()] ~build:latest_build.Builder_db.Build.uuid ();
[txtf "%a" pp_ptime latest_build.Builder_db.Build.start]; a_class ["link"]
H.txt " "; ]
[txtf "%a" pp_ptime latest_build.Builder_db.Build.start]; ];
div ~a:[a_class [""]]
[ artifact ~basename:true ~job_name ~build:latest_build ~file:latest_artifact ];
];
] ]
@ artifact
~basename:true
~job_name
~build:latest_build
~file:latest_artifact
@ [ H.br () ]
let make_jobs jobs = let make_jobs jobs =
jobs |> List.map (fun (job_name, synopsis, platform_builds) -> H.div
H.li ( ~a:[H.a_class ["min-w-full"]]
(List.map (fun (job_name, synopsis, platform_builds) ->
H.div ~a:[H.a_class ["md:grid md:grid-cols-4 divide-y dark:divide-gray-200 divide-gray-600"]]
[ [
H.a ~a:H.[a_href ("/job/" ^ job_name ^ "/")] H.div
~a:[H.a_class ["px-6 py-4 font-medium md:col-span-1"]]
[
H.a ~a:[H.a_href ("/job/" ^ job_name ^ "/"); H.a_class ["link font-bold"]]
[H.txt job_name]; [H.txt job_name];
H.br(); H.br();
H.txt (Option.value ~default:"" synopsis); H.txt (Option.value ~default:"" synopsis);
H.br () ];
]
@ List.concat_map (make_platform_builds ~job_name) platform_builds H.div
) ~a:[H.a_class ["px-4 py-4 text-gray-400 block md:col-span-3"]]
[
H.div ~a:[H.a_class ["md:flex flex-col wrap"]]
(List.concat_map (make_platform_builds ~job_name) platform_builds);
];
];
) )
jobs)
let make_body section_job_map = let make_body section_job_map =
let aux section jobs acc = let aux section jobs acc =
acc @ [ acc @ [
H.h2 [ H.txt section ]; H.div ~a:[H.a_class ["my-4 py-4"]] [
H.ul (make_jobs jobs) H.h2 ~a:[H.a_class ["uppercase font-bold text-2xl"]] [ H.txt section ];
make_jobs jobs;
]
] ]
in in
Utils.String_map.fold aux section_job_map [] Utils.String_map.fold aux section_job_map []
let make_failed_builds = let make_failed_builds =
[ H.p [ [ H.div ~a:H.[a_class ["flex justify-center my-4"]] [
H.txt "View the latest failed builds "; H.a ~a:H.[a_href "/failed-builds";
H.a ~a:H.[a_href "/failed-builds"] a_class ["link-red font-semibold"]]
[H.txt "here"]; [H.txt "View Latest Failed Builds"];
H.txt ".";
]] ]]
let make_all_or_active all = let make_all_or_active all =
[ H.p [ [ H.div ~a:H.[a_class ["flex justify-center my-4"]] [
H.txt (if all then "View active jobs " else "View all jobs "); H.a ~a:H.[a_href (if all then "/" else "/all-builds");
H.a ~a:H.[a_href (if all then "/" else "/all-builds")] a_class ["link font-semibold"]]
[H.txt "here"]; [H.txt (if all then "View Active Jobs" else "View All Jobs")];
H.txt ".";
]] ]]
let make ~all section_job_map = let make ~all section_job_map =
@ -374,6 +420,7 @@ have questions or suggestions.
@ make_failed_builds @ make_failed_builds
@ make_all_or_active all) @ make_all_or_active all)
let make_json ~all:_ section_job_map = let make_json ~all:_ section_job_map =
let all_jobs = let all_jobs =
Utils.String_map.fold Utils.String_map.fold
@ -396,39 +443,42 @@ end
module Job = struct module Job = struct
let make_header ~job_name ~platform ~readme = let make_header ~job_name ~platform ~readme =
H.h1 [txtf "Job %s %a" job_name pp_platform platform] H.h1 ~a:[H.a_class ["text-4xl font-bold text-center my-4 py-4"]] [txtf "Job %s %a" job_name pp_platform platform]
:: ( :: (
match readme with match readme with
| None -> [] | None -> []
| Some data -> | Some data ->
[ [
H.h2 ~a:H.[a_id "readme"] [H.txt "README"]; H.div ~a:[H.a_class ["flex justify-between items-center"]] [
H.a ~a:H.[a_href "#builds"] [H.txt "Skip to builds"]; H.h2 ~a:[H.a_id "readme";] [H.txt "README"];
H.a ~a:[H.a_href "#builds"; H.a_class ["link"]] [H.txt "Skip to builds"];
];
H.Unsafe.data (Utils.md_to_html ~adjust_heading:2 data) H.Unsafe.data (Utils.md_to_html ~adjust_heading:2 data)
] ]
) )
let make_build ~job_name (build, main_binary) = let make_build ~job_name (build, main_binary) =
H.li ( H.li ~a:[H.a_class ["my-4 p-4 border-t-1"]] (
[ [
H.div ~a:[H.a_class ["flex my-2"]] [
check_icon build.Builder_db.Build.result; check_icon build.Builder_db.Build.result;
txtf " %s " build.platform; H.p ~a:[H.a_class ["text-xl px-2"]] [txtf " %s " build.platform;];
H.a ~a:H.[ ];
a_href @@ Link.Job_build.make H.a ~a:[
H.a_href @@ Link.Job_build.make
~job_name ~job_name
~build:build.Builder_db.Build.uuid () ] ~build:build.Builder_db.Build.uuid (); H.a_class ["link"] ]
[ [
txtf "%a" pp_ptime build.Builder_db.Build.start; txtf "%a" pp_ptime build.Builder_db.Build.start;
]; ];
H.txt " ";
] ]
@ match main_binary with @ match main_binary with
| Some main_binary -> | Some main_binary ->
artifact [artifact
~basename:true ~basename:true
~job_name ~job_name
~build ~build
~file:main_binary ~file:main_binary]
| None -> | None ->
[ txtf "Build failure: %a" Builder.pp_execution_result [ txtf "Build failure: %a" Builder.pp_execution_result
build.Builder_db.Build.result ] build.Builder_db.Build.result ]
@ -436,8 +486,10 @@ module Job = struct
let make_builds ~failed ~job_name ~platform builds = let make_builds ~failed ~job_name ~platform builds =
[ [
H.h2 ~a:H.[a_id "builds"] [H.txt "Builds"]; H.div ~a:[H.a_class ["flex justify-between items-center"]] [
H.a ~a:H.[a_href "#readme"] [H.txt "Back to readme"]; H.h2 ~a:[H.a_id "builds"] [H.txt "Builds"];
H.a ~a:[H.a_href "#readme"; H.a_class ["link"]] [H.txt "Back to readme"];
];
H.ul (builds |> List.map (make_build ~job_name)); H.ul (builds |> List.map (make_build ~job_name));
let queries = let queries =
platform |> Option.map (fun p -> `Platform p) |> Option.to_list platform |> Option.map (fun p -> `Platform p) |> Option.to_list
@ -446,7 +498,7 @@ module Job = struct
H.p [ H.p [
H.txt "Excluding failed builds " ; H.txt "Excluding failed builds " ;
H.a ~a:H.[ H.a ~a:H.[
a_href @@ Link.Job.make ~job_name ~queries () a_href @@ Link.Job.make ~job_name ~queries (); a_class ["link"]
] ]
[H.txt "here"] ; [H.txt "here"] ;
H.txt "." ] H.txt "." ]
@ -454,7 +506,7 @@ module Job = struct
H.p [ H.p [
H.txt "Including failed builds " ; H.txt "Including failed builds " ;
H.a ~a:H.[ H.a ~a:H.[
a_href @@ Link.Job.make_failed ~job_name ~queries () a_href @@ Link.Job.make_failed ~job_name ~queries (); a_class ["link-red"]
] ]
[H.txt "here"] ; [H.txt "here"] ;
H.txt "." ] H.txt "." ]
@ -532,15 +584,15 @@ module Job_build = struct
let sha256_hex = Ohex.encode file.sha256 in let sha256_hex = Ohex.encode file.sha256 in
[ [
H.dt [ H.dt [
H.a ~a:H.[a_href @@ Link.Job_build_artifact.make H.a ~a:[H.a_href @@ Link.Job_build_artifact.make
~job_name ~job_name
~build:build_uuid ~build:build_uuid
~artifact:(`File file.filepath) () ~artifact:(`File file.filepath) ();
H.a_class ["link"]
] ]
[H.code [txtf "%a" Fpath.pp file.filepath]] ]; [H.code [txtf "%a" Fpath.pp file.filepath; txtf " (%a)" Fmt.byte_size file.size]] ];
H.dd ([ H.dd ([
H.code [H.txt "SHA256:"; H.txt sha256_hex]; H.code ~a:[H.a_class ["wrap"]] [H.txt "SHA256:"; H.txt sha256_hex];
txtf " (%a)" Fmt.byte_size file.size;
] @ ] @
match main_binary, solo5_manifest with match main_binary, solo5_manifest with
| Some main_binary, Some solo5_manifest when main_binary = file -> | Some main_binary, Some solo5_manifest when main_binary = file ->
@ -550,7 +602,7 @@ module Job_build = struct
in in
[ [
H.h3 [H.txt "Build artifacts"]; H.h3 [H.txt "Build artifacts"];
H.dl (List.concat_map aux artifacts) H.dl ~a:[H.a_class ["p-4 my-4"]] (List.concat_map aux artifacts)
PixieDust marked this conversation as resolved Outdated

I have intentionally not used local opens as there are just too many things in Tyxml.Html that can shadow things inadverdently.

I have intentionally not used local opens as there are just too many things in Tyxml.Html that can shadow things inadverdently.
] ]
let make_reproductions let make_reproductions
@ -581,6 +633,7 @@ module Job_build = struct
different_input_same_output different_input_same_output
in in
[ [
H.div ~a:[H.a_class ["my-4"]] [
H.h3 [ H.h3 [
txtf "Reproduced by %d builds" txtf "Reproduced by %d builds"
(List.length (same_input_same_output @ different_input_same_output))]; (List.length (same_input_same_output @ different_input_same_output))];
@ -589,6 +642,7 @@ module Job_build = struct
@ different_input_same_output_html @ different_input_same_output_html
) )
] ]
]
let make_not_reproducible let make_not_reproducible
~(build:Builder_db.Build.t) ~(build:Builder_db.Build.t)
@ -626,7 +680,7 @@ module Job_build = struct
H.a ~a:[ H.a ~a:[
H.a_href @@ Link.Compare_builds.make H.a_href @@ Link.Compare_builds.make
~left:b.uuid ~left:b.uuid
~right:build.uuid () ] ~right:build.uuid () ; H.a_class ["link"] ]
[txtf "%a" pp_ptime b.start]] [txtf "%a" pp_ptime b.start]]
] ]
| _ -> [] | _ -> []
@ -651,27 +705,47 @@ module Job_build = struct
~latest ~next ~previous ~latest ~next ~previous
= =
[ [
H.h2 ~a:H.[a_id "build"] [txtf "Build %a" pp_ptime build.start]; H.h2 ~a:[H.a_id "build";] [txtf "Build %a" pp_ptime build.start];
H.p [txtf "Built on platform %s" build.platform ]; H.div ~a:[H.a_class []] [
H.p [txtf "Build took %a." Ptime.Span.pp delta ]; H.table
H.p [txtf "Execution result: %a." Builder.pp_execution_result build.result]; ~thead:
H.h3 [H.txt "Build info"]; (H.thead
H.ul [ [
H.li [ H.tr ~a:[H.a_class ["border"]]
H.a ~a:H.[ [
a_href @@ Link.Job_build_artifact.make H.th [ H.txt "Platform" ];
~job_name H.th [ H.txt "Duration" ];
~build:build.uuid H.th [ H.txt "Execution Result" ];
~artifact:`Console ()
] [H.txt "Console output"];
]; ];
H.li [ ])
H.a ~a:H.[ [
a_href @@ Link.Job_build_artifact.make H.tr ~a:[H.a_class ["text-center border"]]
[
H.td [ txtf "%s" build.platform ];
H.td [ txtf "%a." Ptime.Span.pp delta ];
H.td [ txtf "%a" Builder.pp_execution_result build.result ];
]
];
];
H.h3 [H.txt "Build info"];
H.div ~a:[H.a_class ["my-4 md:flex justify-between items-center"]] [
H.div [
H.a ~a:[
H.a_href @@ Link.Job_build_artifact.make
~job_name ~job_name
~build:build.uuid ~build:build.uuid
~artifact:`Script () ~artifact:`Console ();
] [H.txt "Build script"]; H.a_class ["link"]
] [H.txt "Console output -->"];
];
H.div [
H.a ~a:[
H.a_href @@ Link.Job_build_artifact.make
~job_name
~build:build.uuid
~artifact:`Script ();
H.a_class ["link"]
] [H.txt "Build script -->"];
] ]
]; ];
] ]
@ -811,22 +885,17 @@ and the rest of the unaccounted data.\
~same_input_different_output ~same_input_different_output
~latest ~next ~previous ~latest ~next ~previous
in in
let style_grid = H.a_style "display: flex; " in
let style_col_left =
H.a_style "width: 45em; min-width: 43em;" in
let style_col_right = H.a_style "width: 50%" in
let body = [ let body = [
H.h1 [txtf "Job %s" job_name]; H.h1 ~a:[H.a_class ["text-4xl font-bold text-center"]] [txtf "Job %s" job_name];
H.div~a:[ style_grid ] [ H.div~a:[ H.a_class ["md:grid grid-cols-2 gap-8"] ] [
H.div~a:[ style_col_left ] left_column; H.div~a:[ ] left_column;
H.div~a:[ style_col_right ] right_column H.div~a:[ ] right_column
] ]
] ]
in in
layout layout
~nav:(`Build (job_name, build)) ~nav:(`Build (job_name, build))
~title:(Fmt.str "Job %s %a" job_name pp_ptime build.start) ~title:(Fmt.str "Job %s %a" job_name pp_ptime build.start)
~manual_width:true
body body
end end
@ -865,8 +934,7 @@ let duniverse_diffs diffs =
let opam_diffs diffs = let opam_diffs diffs =
List.concat_map (fun pd -> List.concat_map (fun pd ->
H.h4 [ txtf "%a" Opamdiff.pp_opam_diff pd ] :: H.h4 ~a:[H.a_class ["text-md text-primary-500"]] [ txtf "%a" Opamdiff.pp_opam_diff pd ] ::
H.h5 [ H.txt "diff" ] ::
H.pre [ H.code [H.txt pd.diff] ] :: H.pre [ H.code [H.txt pd.diff] ] ::
H.br () :: []) H.br () :: [])
diffs diffs
@ -886,41 +954,41 @@ let compare_builds
if amount = 0 then if amount = 0 then
items, data items, data
else else
H.li [ H.a ~a:[H.a_href id_href] [txtf "%d %s" amount txt] ] :: items, H.li [ H.a ~a:[H.a_href id_href; H.a_class ["link"]] [txtf "%d %s" amount txt] ] :: items,
data @ H.h3 ~a:[H.a_id id] [H.txt txt] :: code) data @ H.h3 ~a:[H.a_id id;] [H.txt txt] :: code)
([], []) ([], [])
([ ("opam-packages-removed", "Opam packages removed", ([ ("opam-packages-removed", "Opam packages removed",
OpamPackage.Set.cardinal left, [ H.code (packages left) ]) ; OpamPackage.Set.cardinal left, [ H.code ~a:[H.a_class ["code-diff"]] (packages left) ]) ;
("opam-packages-installede", "New opam packages installed", ("opam-packages-installede", "New opam packages installed",
OpamPackage.Set.cardinal right, [ H.code (packages right) ]) ; OpamPackage.Set.cardinal right, [ H.code ~a:[H.a_class ["code-diff"]] (packages right) ]) ;
("opam-packages-version-diff", "Opam packages with version changes", ("opam-packages-version-diff", "Opam packages with version changes",
List.length version_diff, [ H.code (package_diffs version_diff) ]) ; List.length version_diff, [ H.code ~a:[H.a_class ["code-diff"]] (package_diffs version_diff) ]) ;
] @ (match duniverse with ] @ (match duniverse with
| Ok (duniverse_left, duniverse_right, duniverse_content_diff) -> | Ok (duniverse_left, duniverse_right, duniverse_content_diff) ->
[ [
("duniverse-dirs-removed", "Duniverse directories removed", ("duniverse-dirs-removed", "Duniverse directories removed",
List.length duniverse_left, [ H.code (duniverse_dirs duniverse_left) ]) ; List.length duniverse_left, [ H.code ~a:[H.a_class ["code-diff"]] (duniverse_dirs duniverse_left) ]) ;
("duniverse-dirs-installed", "New duniverse directories installed", ("duniverse-dirs-installed", "New duniverse directories installed",
List.length duniverse_right, [ H.code (duniverse_dirs duniverse_right) ]) ; List.length duniverse_right, [ H.code ~a:[H.a_class ["code-diff"]] (duniverse_dirs duniverse_right) ]) ;
("duniverse-dirs-content-diff", "Duniverse directories with content changes", ("duniverse-dirs-content-diff", "Duniverse directories with content changes",
List.length duniverse_content_diff, [ H.code (duniverse_diffs duniverse_content_diff) ]) ; List.length duniverse_content_diff, [ H.code ~a:[H.a_class ["code-diff"]] (duniverse_diffs duniverse_content_diff) ]) ;
] ]
| Error `Msg msg -> [ "duniverse-dirs-error", "Duniverse parsing error", 1, [ H.txt msg ] ] | Error `Msg msg -> [ "duniverse-dirs-error", "Duniverse parsing error", 1, [ H.txt msg ] ]
) @ [ ) @ [
("opam-packages-opam-diff", "Opam packages with changes in their opam file", ("opam-packages-opam-diff", "Opam packages with changes in their opam file",
List.length opam_diff, opam_diffs opam_diff) ; List.length opam_diff, opam_diffs opam_diff) ;
("env-removed", "Environment variables removed", ("env-removed", "Environment variables removed",
List.length removed_env, [ H.code (key_values removed_env) ]) ; List.length removed_env, [ H.code ~a:[H.a_class ["code-diff"]] (key_values removed_env) ]) ;
("env-added", "New environment variables added", ("env-added", "New environment variables added",
List.length added_env, [ H.code (key_values added_env) ]) ; List.length added_env, [ H.code ~a:[H.a_class ["code-diff"]] (key_values added_env) ]) ;
("env-changed", "Environment variables changed", ("env-changed", "Environment variables changed",
List.length changed_env, [ H.code (key_value_changes changed_env) ]) ; List.length changed_env, [ H.code ~a:[H.a_class ["code-diff"]] (key_value_changes changed_env) ]) ;
("pkgs-removed", "System packages removed", ("pkgs-removed", "System packages removed",
List.length removed_pkgs, [ H.code (key_values removed_pkgs) ]) ; List.length removed_pkgs, [ H.code ~a:[H.a_class ["code-diff"]] (key_values removed_pkgs) ]) ;
("pkgs-added", "New system packages added", ("pkgs-added", "New system packages added",
List.length added_pkgs, [ H.code (key_values added_pkgs) ]) ; List.length added_pkgs, [ H.code ~a:[H.a_class ["code-diff"]] (key_values added_pkgs) ]) ;
("pkgs-changed", "System packages changed", ("pkgs-changed", "System packages changed",
List.length changed_pkgs, [ H.code (key_value_changes changed_pkgs) ]) ; List.length changed_pkgs, [ H.code ~a:[H.a_class ["code-diff"]] (key_value_changes changed_pkgs) ]) ;
]) ])
in in
layout layout
@ -928,31 +996,31 @@ let compare_builds
~title:(Fmt.str "Comparing builds %a and %a" ~title:(Fmt.str "Comparing builds %a and %a"
Uuidm.pp build_left.uuid Uuidm.pp build_right.uuid) Uuidm.pp build_left.uuid Uuidm.pp build_right.uuid)
([ ([
H.h1 [H.txt "Comparing builds"]; H.h1 ~a:[H.a_class ["text-center"]] [H.txt "Comparing builds"];
H.h2 [ H.h2 ~a:[H.a_class ["text-center"]] [
H.txt "Builds "; H.txt "Builds ";
H.a ~a:H.[ a_href @@ (H.a ~a:[ H.a_href @@
Link.Job_build.make Link.Job_build.make
~job_name:job_left ~job_name:job_left
~build:build_left.uuid () ] ~build:build_left.uuid (); H.a_class ["link"] ]
[ txtf "%s@%a %a" [ txtf "%s@%a %a"
job_left job_left
pp_ptime build_left.start pp_ptime build_left.start
pp_platform (Some build_left.platform)]; pp_platform (Some build_left.platform)]);
H.txt " and "; H.txt " and ";
H.a ~a:H.[ a_href @@ H.a ~a:[ H.a_href @@
Link.Job_build.make Link.Job_build.make
~job_name:job_right ~job_name:job_right
~build:build_right.uuid () ] ~build:build_right.uuid (); H.a_class ["link"] ]
[ txtf "%s@%a %a" [ txtf "%s@%a %a"
job_right job_right
pp_ptime build_right.start pp_ptime build_right.start
pp_platform (Some build_right.platform)]; pp_platform (Some build_right.platform)];
]; ];
H.h3 [ H.a ~a:H.[ H.h3 ~a:[H.a_class ["text-right"]] [ H.a ~a:[
a_href @@ Link.Compare_builds.make H.a_href @@ Link.Compare_builds.make
~left:build_right.uuid ~left:build_right.uuid
~right:build_left.uuid () ] ~right:build_left.uuid (); H.a_class ["link"] ]
[H.txt "Compare in reverse direction"]] ; [H.txt "Compare in reverse direction"]] ;
H.ul (List.rev items) ] @ data) H.ul (List.rev items) ] @ data)

View file

@ -4,7 +4,8 @@
remote_instance=https://builds.robur.coop remote_instance=https://builds.robur.coop
local_user_pass=test:test local_user_pass=test:test
local_instance="http://${local_user_pass}@localhost:3000" local_instance="http://${local_user_pass}@localhost:3000"
limit=100 global_limit=100
per_job_limit=3 # Limit to 3 builds per job
curl_json () { curl_json () {
curl --silent --fail --location --header "Accept: application/json" "$@" curl --silent --fail --location --header "Accept: application/json" "$@"
@ -12,10 +13,11 @@ curl_json () {
curl_json "${remote_instance}/" | jq -r .jobs[] | { curl_json "${remote_instance}/" | jq -r .jobs[] | {
while read -r job_name; do while read -r job_name; do
build_count=0
curl_json "${remote_instance}/job/${job_name}" | jq -r .builds[].uuid | { curl_json "${remote_instance}/job/${job_name}" | jq -r .builds[].uuid | {
while read -r build_uuid; do while read -r build_uuid; do
if [ "$limit" -eq 0 ]; then if [ "$global_limit" -eq 0 ] || [ "$build_count" -ge "$per_job_limit" ]; then
break 2; break 2
fi fi
dest=$(mktemp "builder-${build_uuid}.XXXXXXXXXX") dest=$(mktemp "builder-${build_uuid}.XXXXXXXXXX")
curl --silent --fail "${remote_instance}/job/${job_name}/build/${build_uuid}/exec" > "$dest" && { curl --silent --fail "${remote_instance}/job/${job_name}/build/${build_uuid}/exec" > "$dest" && {
@ -23,7 +25,8 @@ curl_json "${remote_instance}/" | jq -r .jobs[] | {
curl --data-binary "@${dest}" "${local_instance}/upload" curl --data-binary "@${dest}" "${local_instance}/upload"
} }
rm -f "$dest" rm -f "$dest"
limit=$((limit - 1)) global_limit=$((global_limit - 1))
build_count=$((build_count + 1))
done done
} }
done done