Compare commits

..

No commits in common. "909798eb05db578082e7a86672efd30157cdf698" and "a6013bef3e5bec0d3269e19e5cabcb9e94ceaee5" have entirely different histories.

9 changed files with 156 additions and 624 deletions

View file

@ -1,14 +1,3 @@
* {
box-sizing: border-box;
}
:root {
--fg: #cdd6f4;
--bg: #11111b;
--link: #bd93f9;
--overlay: #313244;
}
.skip { .skip {
position: absolute; position: absolute;
left: -10000px; left: -10000px;
@ -25,47 +14,33 @@
} }
body { body {
max-width: 50rem; max-width: 45rem;
padding: 2rem; padding: 0 2rem;
margin: 0 auto; margin: 2rem auto 0;
line-height: 1.8; line-height: 1.6;
background: var(--bg); background: #11111b;
color: var(--fg); color: white;
font-family: serif;
} }
h1, h1, h2, h3 {
h2, margin-bottom: 0.25em;
h3,
h4 {
margin-block-end: 0.25em;
line-height: 1.3; line-height: 1.3;
font-family: sans-serif;
a,
a:visited {
color: var(--fg);
text-decoration: none;
}
a:hover {
color: var(--link);
text-decoration: underline;
}
}
ul {
margin-block-start: 0;
} }
p { p {
margin-block-start: 0; margin-top: 0;
text-align: justify; }
hyphens: auto;
p + p {
text-indent: 2em;
}
ul {
margin-top: 0;
} }
#main-nav { #main-nav {
line-height: 1.3; font-size: 125%;
} }
#main-nav ul { #main-nav ul {
@ -75,14 +50,24 @@ p {
#main-nav li { #main-nav li {
display: inline-block; display: inline-block;
margin-inline-end: 0.5em; }
#main-nav li::before {
content: '/ ';
}
#main-nav li:first-child::before {
content: '';
} }
#site-title { #site-title {
font-family: sans-serif;
font-weight: bold; font-weight: bold;
font-size: 150%; color: inherit;
margin-bottom: 0.25rem; text-decoration: none;
}
#site-title:hover {
text-decoration: underline;
} }
footer { footer {
@ -97,26 +82,6 @@ header {
margin-bottom: 1em; margin-bottom: 1em;
} }
a, a, a:visited {
a:visited { color: #8ef;
color: var(--link);
}
hr {
border: none;
height: 1px;
background: var(--overlay);
}
pre code {
background: black;
border: 1px solid var(--overlay);
border-radius: 8px;
padding: 1em;
display: block;
overflow-x: auto;
}
code {
font-size: 125%;
} }

View file

@ -4,7 +4,7 @@
--cyan: #8be9fd; --cyan: #8be9fd;
--green: #50fa7b; --green: #50fa7b;
--orange: #ffb86c; --orange: #ffb86c;
--pink: #ff79c6; --pink: #f5a;
--purple: #bd93f9; --purple: #bd93f9;
--red: #f55; --red: #f55;
--yellow: #f1fa8c; --yellow: #f1fa8c;
@ -22,134 +22,49 @@
--yellow: #df8e1d; --yellow: #df8e1d;
} }
} */ } */
/* Background */ /* Background */ .bg,
.bg, /* PreWrapper */ .chroma { background-color:var(--chroma-bg); }
/* PreWrapper */ .chroma { /* Other */ .chroma .x { }
background-color: var(--chroma-bg); /* Error */ .chroma .err { }
} /* CodeLine */ .chroma .cl { }
/* Other */ /* LineLink */ .chroma .lnlinks { outline:none;text-decoration:none;color:inherit }
.chroma .x { /* LineTableTD */ .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; }
} /* LineTable */ .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; }
/* Error */ /* LineHighlight */ .chroma .hl { background-color:#3d3f4a }
.chroma .err { /* LineNumbersTable */ .chroma .lnt,
} /* LineNumbers */ .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:var(--comment) }
/* CodeLine */ /* Line */ .chroma .line { display:flex; }
.chroma .cl { /* Keyword */ .chroma .k,
}
/* LineLink */
.chroma .lnlinks {
outline: none;
text-decoration: none;
color: inherit;
}
/* LineTableTD */
.chroma .lntd {
vertical-align: top;
padding: 0;
margin: 0;
border: 0;
}
/* LineTable */
.chroma .lntable {
border-spacing: 0;
padding: 0;
margin: 0;
border: 0;
}
/* LineHighlight */
.chroma .hl {
background-color: #3d3f4a;
}
/* LineNumbersTable */
.chroma .lnt,
/* LineNumbers */ .chroma .ln {
white-space: pre;
-webkit-user-select: none;
user-select: none;
margin-right: 0.4em;
padding: 0 0.4em 0 0.4em;
color: var(--comment);
}
/* Line */
.chroma .line {
display: flex;
}
/* Keyword */
.chroma .k,
/* KeywordConstant */ .chroma .kc, /* KeywordConstant */ .chroma .kc,
/* KeywordNamespace */ .chroma .kn, /* KeywordNamespace */ .chroma .kn,
/* KeywordPseudo */ .chroma .kp, /* KeywordPseudo */ .chroma .kp,
/* KeywordReserved */ .chroma .kr { /* KeywordReserved */ .chroma .kr { color:var(--pink) }
color: var(--pink); /* KeywordType */ .chroma .kt { color:var(--cyan) }
} /* KeywordDeclaration */ .chroma .kd { color:var(--cyan);font-style:italic }
/* KeywordType */ /* Name */ .chroma .n { }
.chroma .kt { /* NameBuiltinPseudo */ .chroma .bp { }
color: var(--cyan); /* NameConstant */ .chroma .no { }
} /* NameDecorator */ .chroma .nd { }
/* KeywordDeclaration */ /* NameEntity */ .chroma .ni { }
.chroma .kd { /* NameException */ .chroma .ne { }
color: var(--cyan); /* NameAttribute */ .chroma .na,
font-style: italic;
}
/* Name */
.chroma .n {
}
/* NameBuiltinPseudo */
.chroma .bp {
}
/* NameConstant */
.chroma .no {
}
/* NameDecorator */
.chroma .nd {
}
/* NameEntity */
.chroma .ni {
}
/* NameException */
.chroma .ne {
}
/* NameAttribute */
.chroma .na,
/* NameClass */ .chroma .nc, /* NameClass */ .chroma .nc,
/* NameFunctionMagic */ .chroma .fm, /* NameFunctionMagic */ .chroma .fm,
/* NameFunction */ .chroma .nf { /* NameFunction */ .chroma .nf { color:var(--green) }
color: var(--green); /* NameNamespace */ .chroma .nn { }
} /* NameOther */ .chroma .nx { }
/* NameNamespace */ /* NameProperty */ .chroma .py { }
.chroma .nn { /* NameTag */ .chroma .nt { color:var(--pink) }
} /* NameBuiltin */ .chroma .nb,
/* NameOther */
.chroma .nx {
}
/* NameProperty */
.chroma .py {
}
/* NameTag */
.chroma .nt {
color: var(--pink);
}
/* NameBuiltin */
.chroma .nb,
/* NameLabel */ .chroma .nl, /* NameLabel */ .chroma .nl,
/* NameVariable */ .chroma .nv, /* NameVariable */ .chroma .nv,
/* NameVariableClass */ .chroma .vc, /* NameVariableClass */ .chroma .vc,
/* NameVariableGlobal */ .chroma .vg, /* NameVariableGlobal */ .chroma .vg,
/* NameVariableInstance */ .chroma .vi { /* NameVariableInstance */ .chroma .vi { color:var(--cyan);font-style:italic }
color: var(--cyan); /* NameVariableMagic */ .chroma .vm { }
font-style: italic; /* Literal */ .chroma .l { }
} /* LiteralDate */ .chroma .ld { }
/* NameVariableMagic */ /* LiteralString */ .chroma .s,
.chroma .vm {
}
/* Literal */
.chroma .l {
}
/* LiteralDate */
.chroma .ld {
}
/* LiteralString */
.chroma .s,
/* LiteralStringAffix */ .chroma .sa, /* LiteralStringAffix */ .chroma .sa,
/* LiteralStringBacktick */ .chroma .sb, /* LiteralStringBacktick */ .chroma .sb,
/* LiteralStringChar */ .chroma .sc, /* LiteralStringChar */ .chroma .sc,
@ -162,85 +77,34 @@
/* LiteralStringOther */ .chroma .sx, /* LiteralStringOther */ .chroma .sx,
/* LiteralStringRegex */ .chroma .sr, /* LiteralStringRegex */ .chroma .sr,
/* LiteralStringSingle */ .chroma .s1, /* LiteralStringSingle */ .chroma .s1,
/* LiteralStringSymbol */ .chroma .ss { /* LiteralStringSymbol */ .chroma .ss { color:var(--yellow) }
color: var(--yellow); /* LiteralNumber */ .chroma .m,
}
/* LiteralNumber */
.chroma .m,
/* LiteralNumberBin */ .chroma .mb, /* LiteralNumberBin */ .chroma .mb,
/* LiteralNumberFloat */ .chroma .mf, /* LiteralNumberFloat */ .chroma .mf,
/* LiteralNumberHex */ .chroma .mh, /* LiteralNumberHex */ .chroma .mh,
/* LiteralNumberInteger */ .chroma .mi, /* LiteralNumberInteger */ .chroma .mi,
/* LiteralNumberIntegerLong */ .chroma .il, /* LiteralNumberIntegerLong */ .chroma .il,
/* LiteralNumberOct */ .chroma .mo { /* LiteralNumberOct */ .chroma .mo { color:var(--purple) }
color: var(--purple); /* Operator */ .chroma .o,
} /* OperatorWord */ .chroma .ow { color:var(--pink) }
/* Operator */ /* Punctuation */ .chroma .p { }
.chroma .o, /* Comment */ .chroma .c,
/* OperatorWord */ .chroma .ow {
color: var(--pink);
}
/* Punctuation */
.chroma .p {
}
/* Comment */
.chroma .c,
/* CommentHashbang */ .chroma .ch, /* CommentHashbang */ .chroma .ch,
/* CommentMultiline */ .chroma .cm, /* CommentMultiline */ .chroma .cm,
/* CommentSingle */ .chroma .c1, /* CommentSingle */ .chroma .c1,
/* CommentSpecial */ .chroma .cs { /* CommentSpecial */ .chroma .cs { color:var(--comment) }
color: var(--comment); /* CommentPreproc */ .chroma .cp,
} /* CommentPreprocFile */ .chroma .cpf { color:var(--pink) }
/* CommentPreproc */ /* Generic */ .chroma .g { }
.chroma .cp, /* GenericDeleted */ .chroma .gd { color:var(--red) }
/* CommentPreprocFile */ .chroma .cpf { /* GenericEmph */ .chroma .ge { text-decoration:underline }
color: var(--pink); /* GenericError */ .chroma .gr { }
} /* GenericHeading */ .chroma .gh { font-weight:bold }
/* Generic */ /* GenericInserted */ .chroma .gi { color:var(--green);font-weight:bold }
.chroma .g { /* GenericOutput */ .chroma .go { color:var(--comment) }
} /* GenericPrompt */ .chroma .gp { }
/* GenericDeleted */ /* GenericStrong */ .chroma .gs { }
.chroma .gd { /* GenericSubheading */ .chroma .gu { font-weight:bold }
color: var(--red); /* GenericTraceback */ .chroma .gt { }
} /* GenericUnderline */ .chroma .gl { text-decoration:underline }
/* GenericEmph */ /* TextWhitespace */ .chroma .w { }
.chroma .ge {
text-decoration: underline;
}
/* GenericError */
.chroma .gr {
}
/* GenericHeading */
.chroma .gh {
font-weight: bold;
}
/* GenericInserted */
.chroma .gi {
color: var(--green);
font-weight: bold;
}
/* GenericOutput */
.chroma .go {
color: var(--comment);
}
/* GenericPrompt */
.chroma .gp {
color: var(--comment);
}
/* GenericStrong */
.chroma .gs {
}
/* GenericSubheading */
.chroma .gu {
font-weight: bold;
}
/* GenericTraceback */
.chroma .gt {
}
/* GenericUnderline */
.chroma .gl {
text-decoration: underline;
}
/* TextWhitespace */
.chroma .w {
}

View file

@ -1,295 +0,0 @@
+++
title = "Zosimos Part 1: Workflow Setup"
date = 2025-03-31T12:00:00-04:00
tags = ["osdev", "zig", "raspberry pi"]
+++
Programming my own OS has been a dream of mine for years, but my attempts always
end up going the same way: writing some code that can run in a virtual machine,
learning some interesting stuff about x86_64, and then giving up before I even
reach userspace. Now I find myself called again by the siren song, but I'm
determined to not go down the same road again ... so I'm doing it on an arm
machine this time. I ended up settling on the Raspberry Pi 4B, since the price
was reasonable and the large community makes it easy to find answers to any
question I might have. But before I can set out to build the greatest OS ever,
I need to set up a good workflow for building and testing.
## Building the Kernel
For this go around, I decided to write my kernel in Zig. I've gotten too used to
the conveniences of a modern language to go back to C, and while I love Rust
dearly I've found myself frustrated by having to use shell scripts and make to
take care of the parts of getting to a bootable image that Cargo doesn't concern
itself with. Zig's build system is flexible enough to handle the whole process
itself, and while nothing in this post is particularly difficult, I have reason
to believe it'll keep up even as the complexity increases.
I won't go into detail about the actual code since 1\. it's extremely trivial
and 2\. it's not the main focus of this post. All that matters is that it
consists of some Zig code, an assembly stub, and a linker script to put it all
together. In our `build.zig` file we therefore write:
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOption(.{});
const optimize = b.standardOptimizeOption(.{});
const kernel = b.addExecutable(.{
.name = "kernel",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
kernel.setLinkerScript(b.path("src/Link.ld"));
kernel.addAssemblyFile(b.path("src/startup.s"));
b.installArtifact(kernel);
}
```
As it is, this will try to build our kernel for the host machine and OS.
To get a freestanding binary we need to change `target`:
```zig
const target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .freestanding,
});
```
Now if we run `zig build` we'll see our compiled kernel in `zig-out/bin/kernel`.
But we're not done yet. This is an ELF file, and the Pi only knows how to boot
flat binaries. We'll still keep the ELF around for debugging, but we add the
following to create a flat binary:[^2]
```zig
const kernel_bin = b.addObjCopy(kernel.getEmittedBin(), .{
.format = .bin,
.basename = "kernel.img",
});
const install_kernel = b.addInstallBinFile(
kernel_bin.getOutput(),
"kernel8.img",
);
b.getInstallStep().dependOn(&install_kernel.step);
```
Before we can boot this kernel image though, we need some other stuff. The Pi is
a little strange in that it reads most of its firmware from the boot drive
instead of from flash on the board. Normally we'd just download these firmware
files ourselves, but we can do better!
[^2]: The filename `kernel8.img` is important since it tells the board to boot
up in 64 bit mode. This can be overridden in [`config.txt`][7] if you really
want a different filename.
[1]: https://git.wires.systems/wires/zosimos/src/commit/8466e9b4d2fbca85d53d8dadc87914b4766c43de/src
[7]: https://www.raspberrypi.com/documentation/computers/config_txt.html
## Fetching Firmware
A great thing about the Zig package manager is that it can be used to fetch any
dependencies, not just Zig packages. In this case we're going to use it to grab
the firmware files for the Pi so that we can reference it in our build script.
If we run:
```console
$ zig fetch --save=rpi_firmware 'https://github.com/raspberrypi/firmware/archive/refs/tags/1.20250305.tar.gz'
```
then the latest (at time of writing) release of the firmware will be added to
our project as a dependency. This also provides us a way to ensure the integrity
of the firmware (at least that it hasn't changed since we first fetched it)
since it's saved alongside a hash. Let's adjust the build script to place all
the files we need[^3] in a `boot` directory:
[^3]: I determined this by trial and error, but it's probably documented
somewhere I missed.
```zig
const kernel_bin = b.addObjCopy(kernel.getEmittedBin(), .{
.format = .bin,
.basename = "kernel.img",
});
const install_kernel = b.addInstallFile(
kernel_bin.getOutput(),
"boot/kernel8.img",
);
b.getInstallStep().dependOn(&install_kernel.step);
const firmware = b.dependency("rpi_firmware", .{});
b.installDirectory(.{
.source_dir = firmware.path("boot"),
.install_dir = .prefix,
.install_subdir = "boot",
.include_extensions = &.{
"start4.elf",
"start4db.elf",
"fixup4.dat",
"bcm2711-rpi-4-b.dtb",
},
});
```
## Network Booting
Having to repeatedly remove the memory card, transfer a new kernel to it, and
put it back in sounds like a massive pain. Luckily, the Pi4B supports network
booting from a tftp server. The simplest way to set this up would be to install
the `tftp-hpa` package and then just run the server normally, having it serve
the `zig-out/boot` folder. But I don't like having daemons that I'm only going
to use for a single project installed on my system, so for no real reason other
than aesthetics I'm going to be running the tftp server inside a container. The
Dockerfile is reproduced below in its entirety:
```dockerfile
# docker/tftp/Dockerfile
FROM alpine:3
RUN apk add --no-cache tftp-hpa
ENTRYPOINT ["in.tftpd"]
```
This is combined with the following `compose.yaml`:
```yaml
services:
tftp:
build: ./docker/tftp
ports:
- 69:69/udp
volumes:
- ./zig-out/boot:/data:ro
command: --verbose --foreground --secure /data
```
running `docker compose up` should now start the tftp server properly.[^1]
[^1]: Since tftpd only logs to syslog and the container won't normally have
a syslogd running in it, you'll have to modify the setup if you want to see
logs. I initially did this during testing by using busybox's syslogd in the
container, but found that it made shutdown times very long, which wasn't
ideal when frequently restarting the container.
Lastly, we have to configure the Pi itself. Network boot config info is stored
in the board's EEPROM, so boot into Linux on the Pi and run
```console
# rpi-eeprom-config --edit
```
This will bring up a text editor where you can edit the EEPROM variables in
a `.ini`-like format. We'll write the following config:
```ini
[all]
BOOT_UART=1
BOOT_ORDER=0xf12
TFTP_PREFIX=1
TFTP_IP=x.y.z.w
```
where `TFTP_IP` should be set to the IP of your tftp server. All of these
options are documented [here][2] if you want to know what specifically they do.
[2]: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html
Now, assuming it has a connection, our Pi should successfully boot over the
network! But since all our so-called kernel does at this point is spin forever,
how can we tell that anything's even happening?
## Remote Debugging
The easiest choice would probably be to make the program actually do something,
like blink the activity LED, but I've decided to set up remote debugging
instead. I'm using the [FT2232H mini module][3] for this, since it lets me do
both UART and JTAG over a single USB connection on my dev machine. For an
explanation of how to hook it up to the Pi, see [this blog post][4]. Like the
author of that post I'll be using OpenOCD in a docker container to connect to
the board, but I went about it a little differently. Since 2021, OpenOCD has
included a config for the Pi4B by default, so I only had to write the following
short config for the FT2232H:
```tcl
# docker/openocd/interface/ft2232h.cfg
adapter driver ftdi
ftdi device_desc "FT2232H MiniModule"
ftdi vid_pid 0x0403 0x6010
ftdi layout_init 0x0000 0x000b
ftdi channel 0
```
I'm extremely new to OpenOCD and JTAG in general, so for all I know this might
be terribly broken in some way I just haven't noticed yet, but it's been working
so far. The Dockerfile is then as follows:
```dockerfile
# docker/openocd/Dockerfile
FROM alpine:3
RUN apk add --no-cache openocd
ADD interface/ft2232h.cfg /usr/share/openocd/scripts/interface/
ENTRYPOINT ["openocd"]
```
And we add the following entry under `services` in `compose.yaml`:
```yaml
openocd:
build: ./docker/openocd
ports:
- 6666:6666
- 4444:4444
- 3333:3333
devices:
- "/dev/bus/usb:/dev/bus/usb"
command: -f interface/ft2232h.cfg -f board/rpi4b.cfg -c "bindto 0.0.0.0"
```
Lastly, we need to add a file called `config.txt` with the following contents:
```ini
[all]
gpio=22-27=np
enable_jtag_gpio=1
enable_uart=1
uart_2ndstage=1
```
and make sure to install it in our `build.zig`:
```zig
b.installFile("config.txt", "boot/config.txt");
```
Now we should be able to attach to `/dev/ttyUSB0` to see the UART messages when
we reboot and connect to OpenOCD's GDB server to debug the running kernel.
[3]: https://ftdichip.com/products/ft2232h-mini-module/
[4]: https://vinnie.work/blog/2021-04-02-ft2232h-rpi4#wiring-up-the-hardware
## What's Next?
For the project in general, my next goal is to set up UART communication, and
then hopefully some physical memory management with ideas from [this paper][5].
As far as the specific subject of this post goes, I want to work on making the
startup experience nicer. As it stands right now, OpenOCD is being started at
the same time as the tftp server, when the board is never ready, so the
container always has to be restarted. It would be good to have it wait until
after the board is booted to start trying to connect, but I don't have any great
ideas on how to pull that off right now.
[5]: https://www.usenix.org/system/files/atc23-wrenger.pdf

View file

@ -1,6 +1,6 @@
baseURL = 'https://wires.systems' baseURL = 'https://wires.systems'
languageCode = 'en-us' languageCode = 'en-us'
title = "wires" title = "~wires"
uglyurls = true uglyurls = true
enableGitInfo = true enableGitInfo = true
disableKinds = ['section', 'taxonomy'] disableKinds = ['section', 'taxonomy']
@ -17,11 +17,6 @@ summaryLength = 10
[outputFormats.rss] [outputFormats.rss]
baseName = 'feed' baseName = 'feed'
[[menu.main]]
weight = -10
name = "home"
url = "/"
[[menu.main]] [[menu.main]]
name = "rss" name = "rss"
url = "/feed.xml" url = "/feed.xml"

View file

@ -0,0 +1 @@
<pre class="chroma"><code class="language-{{ .Type }}" data-lang="{{ .Type }}">{{ (transform.HighlightCodeBlock .).Inner }}</code></pre>

View file

@ -1,3 +0,0 @@
<h{{ .Level }} id="{{ .Anchor }}" {{- with .Attributes.class }} class="{{ . }}" {{- end }}>
<a href="#{{ .Anchor }}">{{ .Text }}</a>
</h{{ .Level }}>

View file

@ -1,30 +1,29 @@
<!doctype html> <!DOCTYPE html>
<html lang="{{ .Site.Language.LanguageCode }}"> <html lang="{{ .Site.Language.LanguageCode }}">
<head> <head>
{{ partial "head.html" . }} {{ partial "head.html" . }}
<!-- this is here to make sure that if we can't load CSS the icons don't totally fuck up layout --> <!-- this is here to make sure that if we can't load CSS the icons don't totally fuck up layout -->
<style> <style>.icon svg { width: 1.25em; height: 1.25em; }</style>
.icon svg { {{ range .AlternativeOutputFormats -}}
width: 1.25em; {{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
height: 1.25em; {{ end -}}
}
</style>
{{ range .AlternativeOutputFormats -}} {{ printf `
<link rel="%s" type="%s" href="%s" title="%s" />
` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }} {{ end -}}
</head> </head>
<body> <body>
<a href="#main" class="skip">Skip to content</a> <a href="#main" class="skip">Skip to content</a>
<nav id="main-nav"> <nav id="main-nav">
<div id="site-title">{{.Site.Title}}</div>
<ul> <ul>
{{ with .Site.Menus.main }} {{ range . }} <li><a href="{{.Site.BaseURL}}" id="site-title">{{.Site.Title}}</a></li>
{{ with .Site.Menus.main }}
{{ range . }}
<li><a href="{{ .URL }}">{{ .Name }}</a></li> <li><a href="{{ .URL }}">{{ .Name }}</a></li>
{{ end }} {{ end }} {{ end }}
{{ end }}
</ul> </ul>
</nav> </nav>
<main id="main">{{ block "main" . }}{{ end }}</main> <main id="main">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }} {{ partial "footer.html" . }}
</body> </body>
</html> </html>

View file

@ -6,8 +6,16 @@
{{ with .Keywords }}<meta name="keywords" content="{{ delimit . ", " }}"/>{{ end }} {{ with .Keywords }}<meta name="keywords" content="{{ delimit . ", " }}"/>{{ end }}
<link rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/"/> <link rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/"/>
<link rel="canonical" href="{{ .Permalink }}"/> <link rel="canonical" href="{{ .Permalink }}"/>
{{ range slice "style.css" "syntax.css"}} {{- with resources.Get "style.css" }}
{{- with resources.Get . }} {{- if eq hugo.Environment "development" }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- end }}
{{- end }}
{{- with resources.Get "syntax.css" }}
{{- if eq hugo.Environment "development" }} {{- if eq hugo.Environment "development" }}
<link rel="stylesheet" href="{{ .RelPermalink }}"> <link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }} {{- else }}
@ -16,6 +24,5 @@
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{ end }}
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title> <title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>

View file

@ -1,6 +1,5 @@
{{ with .GetTerms "tags" }} {{ with .GetTerms "tags" }}
<p> <br>
Tags:{{ range $i, $e := . }}{{- if $i -}},{{ end -}}{{ with $e }} tags:
<a class="tag" href="{{.Permalink}}">{{lower .Title}}</a>{{end}}{{ end }} {{ range $i, $e := . }}{{- if $i -}}, {{ end -}}{{ with $e }}<a class="tag" href="{{.Permalink}}">{{lower .Title}}</a>{{end}}{{ end }}
</p>
{{ end }} {{ end }}