Merge pull request 'Release 0.3.0-rc.1' (#236) from release-0.3.0-rc.1 into main

Reviewed-on: vieter/vieter#236
hash-on-upload 0.3.0-rc.1
Jef Roosens 2022-06-10 15:21:55 +02:00
commit 03df62bbc4
94 changed files with 19263 additions and 547 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ gdb.txt
# Generated docs
_docs/
/man/

View File

@ -45,7 +45,7 @@ pipeline:
- export OBJ_PATH="/vieter/commits/$CI_COMMIT_SHA/vieter-$(echo '${PLATFORM}' | sed 's:/:-:g')"
- export SIG_STRING="PUT\n\n$CONTENT_TYPE\n$DATE\n$OBJ_PATH"
- export SIGNATURE=`echo -en $SIG_STRING | openssl sha1 -hmac $S3_PASSWORD -binary | base64`
- export SIGNATURE="$(echo -en $SIG_STRING | openssl sha1 -hmac $S3_PASSWORD -binary | base64)"
- >
curl
--silent

View File

@ -9,7 +9,6 @@ pipeline:
commands:
- apk add git
- make docs
- 'cd docs/public && tar czvf ../../docs.tar.gz *'
api-docs:
image: 'chewingbever/vlang:latest'
@ -17,25 +16,30 @@ pipeline:
group: 'generate'
commands:
- make api-docs
- 'cd src/_docs && tar czvf ../../api-docs.tar.gz *'
deploy-docs:
slate-docs:
image: 'slatedocs/slate'
group: 'generate'
commands:
- cd docs/api
- bundle exec middleman build --clean
archive:
image: 'alpine'
commands:
- cp -r docs/api/build docs/public/api
- 'cd docs/public && tar czvf ../../docs.tar.gz *'
- 'cd ../../src/_docs && tar czvf ../../api-docs.tar.gz *'
when:
event: push
branch: dev
deploy:
image: 'curlimages/curl'
group: 'deploy'
secrets:
- 'site_api_key'
commands:
- 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T docs.tar.gz https://rustybever.be/api/deploy?dir=docs-vieter'
when:
event: push
branch: dev
deploy-api-docs:
image: 'curlimages/curl'
group: 'deploy'
secrets:
- 'site_api_key'
commands:
- 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T api-docs.tar.gz https://rustybever.be/api/deploy?dir=api-docs-vieter'
when:
event: push

View File

@ -14,6 +14,7 @@ pipeline:
commands:
- mc alias set s3/ https://s3.rustybever.be "$S3_USERNAME" "$S3_PASSWORD"
- mc cp -r "s3/vieter/commits/$CI_COMMIT_SHA" .
- mv "$CI_COMMIT_SHA"/vieter-* .
when:
event: tag
@ -23,9 +24,8 @@ pipeline:
- gitea_release_api_key
settings:
base_url: https://git.rustybever.be
files: ${CI_COMMIT_SHA}/*
files: vieter-*
checksum:
- md5
- sha256
title: ${CI_COMMIT_TAG}
when:

View File

@ -0,0 +1,36 @@
platform: 'linux/amd64'
branches:
exclude: [ main ]
depends_on:
- build
skip_clone: true
pipeline:
generate:
image: 'chewingbever/vlang:latest'
pull: true
commands:
- curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64"
- chmod +x vieter
- ./vieter man man
- cd man
# Generate an HTML page from each man page
- for f in $(ls -1 *.1); do mandoc -Thtml -O style=mandoc.css,man=%N.%S.html $f > "$f.html"; done
# Download the mandoc.css file from the official site
- curl -o mandoc.css -L https://mandoc.bsd.lv/mandoc.css
- tar czvf ../man.tar.gz *.html mandoc.css
deploy:
image: 'curlimages/curl'
secrets:
- 'site_api_key'
commands:
- 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T man.tar.gz https://rustybever.be/api/deploy?dir=man-vieter'
when:
event: push
branch: dev

View File

@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev)
## [0.3.0-rc.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-rc.1)
### Added
* Database migrations
* Improved GitRepo & BuildLog API
* Pagination using `limit` & `offset` query params
* GitRepo: filter by repo
* BuildLog: filter by start & end date, repo, exit code & arch
* CLI flags to take advantage of above API improvements
* Added CLI command to generate all man pages
* PKGBUILDs now install man pages
* Hosted CLI man pages ([vieter(1)](https://rustybever.be/man/vieter/vieter.1.html))
* Proper HTTP API docs ([link](https://rustybever.be/docs/vieter/api/))
### Changed
* Packages from target repo are available during builds
* This can be used as a basic way to support AUR dependencies, by adding
the dependencies to the same repository
* Every build now updates its packages first instead of solely relying on the
updated builder image
* Build logs now show commands being executed
### Fixed
* `POST /api/logs` now correctly uses epoch timestamps instead of strings
## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2)
### Added

View File

@ -60,6 +60,11 @@ api-docs:
rm -rf '$(SRC_DIR)/_docs'
cd '$(SRC_DIR)' && v doc -all -f html -m -readme .
.PHONY: man
man: vieter
rm -rf man
./vieter man man
# =====OTHER=====
.PHONY: lint

View File

@ -3,8 +3,9 @@
pkgbase='vieter'
pkgname='vieter'
pkgver='0.3.0_alpha.2'
pkgver='0.3.0_rc.1'
pkgrel=1
pkgdesc="Vieter is a lightweight implementation of an Arch repository server."
depends=('glibc' 'openssl' 'libarchive' 'sqlite')
makedepends=('git' 'vieter-v')
arch=('x86_64' 'aarch64')
@ -17,11 +18,17 @@ build() {
cd "$pkgname"
make prod
# The default CFLAGS for some reason causes vieter to segfault if used
# inside the PKGBUILD. As a workaround, we use tcc to build a debug build
# that does work, so we can generate the manpages.
CFLAGS= LDFLAGS= make man
}
package() {
pkgdesc="Vieter is a lightweight implementation of an Arch repository server."
install -dm755 "$pkgdir/usr/bin"
install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter"
install -dm755 "$pkgdir/usr/share/man/man1"
install -Dm644 "$pkgname/man"/*.1 "$pkgdir/usr/share/man/man1"
}

View File

@ -5,6 +5,7 @@ pkgbase='vieter-git'
pkgname='vieter-git'
pkgver=0.2.0.r25.g20112b8
pkgrel=1
pkgdesc="Vieter is a lightweight implementation of an Arch repository server."
depends=('glibc' 'openssl' 'libarchive' 'sqlite')
makedepends=('git' 'vieter-v')
arch=('x86_64' 'aarch64')
@ -25,11 +26,17 @@ build() {
cd "$pkgname"
make prod
# The default CFLAGS for some reason causes vieter to segfault if used
# inside the PKGBUILD. As a workaround, we use tcc to build a debug build
# that does work, so we can generate the manpages.
CFLAGS= LDFLAGS= make man
}
package() {
pkgdesc="Vieter is a lightweight implementation of an Arch repository server."
install -dm755 "$pkgdir/usr/bin"
install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter"
install -dm755 "$pkgdir/usr/share/man/man1"
install -Dm644 "$pkgname/man"/*.1 "$pkgdir/usr/share/man/man1"
}

View File

@ -22,13 +22,6 @@ a while now. I wanted a fast language that I could code while relaxing, without
having to exert too much mental effort & V seemed like the right choice for
that.
### Compiler
Vieter compiles with the standard Vlang compiler. However, I do maintain a
[mirror](https://git.rustybever.be/Chewing_Bever/v). This is to ensure my CI
does not break without reason, as I control when & how frequently the mirror is
updated to reflect the official repository.
## Features
* Arch repository server
@ -41,20 +34,24 @@ updated to reflect the official repository.
## Building
In order to build Vieter, you'll need a couple of libraries:
Besides a V installer, Vieter also requires the following libraries to work:
* An installation of V
* gc
* libarchive
* openssl
* sqlite3
**NOTE**: if you encounter any issues compiling Vieter using the absolute
latest version of V, it might be because my mirror is missing a specific commit
that causes issues. For this reason, the `make v` command exists which will
clone my compiler in the `v` directory & build it. Afterwards, you can use this
compiler with make by prepending all make commands with `V_PATH=v/v`. If you do
encounter this issue, please let me know so I can update my mirror & the
codebase to fix it!
### Compiler
Vieter compiles with the standard Vlang compiler. However, I do maintain a
[mirror](https://git.rustybever.be/vieter/v). This is to ensure my CI does not
break without reason, as I control when & how frequently the mirror is updated
to reflect the official repository.
If you encounter issues using the latest V compiler, try using my mirror
instead. `make v` will clone the repository & build the mirror. Afterwards,
prepending any make command with `V_PATH=v/v` tells make to use the locally
compiled mirror instead.
## Contributing
@ -65,3 +62,37 @@ If you wish to contribute to the project, please take note of the following:
* Please follow the
[Conventional Commits](https://www.conventionalcommits.org/) style for your
commit messages.
### Writing documentation
The `docs` directory contains a Hugo site consisting of all user &
administrator documentation. `docs/api` on the other hand is a
[Slate](https://github.com/slatedocs/slate) project describing the HTTP web
API.
To modify the Hugo documentation, you'll need to install Hugo. Afterwards, you
can use the following commands inside the `docs` directory:
```sh
# Build the documentation
hugo
# Host an auto-refreshing web server with the documentation. Important to note
# is that the files will be at `http://localhost:1313/docs/vieter` instead of
# just `http://localhost:1313/`
hugo server
```
For the Slate docs, I personally just start a docker container:
```sh
docker run \
--rm \
-p 4567:4567 \
--name slate \
-v $(pwd)/docs/api/source:/srv/slate/source slatedocs/slate serve
```
This will make the Slate docs available at http://localhost:4567. Sadly, this
server doesn't auto-refresh, so you'll have to manually refresh your browser
every time you make a change.

View File

@ -0,0 +1,12 @@
.git/
.github/
build/
.editorconfig
.gitattributes
.gitignore
CHANGELOG.md
CODE_OF_CONDUCT.md
deploy.sh
font-selection.json
README.md
Vagrantfile

View File

@ -0,0 +1,18 @@
# EditorConfig is awesome: https://EditorConfig.org
# Top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.rb]
charset = utf-8
[*.md]
trim_trailing_whitespace = false

1
docs/api/.gitattributes vendored 100644
View File

@ -0,0 +1 @@
source/javascripts/lib/* linguist-vendored

27
docs/api/.gitignore vendored 100644
View File

@ -0,0 +1,27 @@
*.gem
*.rbc
.bundle
.config
coverage
InstalledFiles
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
*.DS_STORE
build/
.cache
.vagrant
.sass-cache
# YARD artifacts
.yardoc
_yardoc
doc/
.idea/
# Vagrant artifacts
ubuntu-*-console.log

13
docs/api/Gemfile 100644
View File

@ -0,0 +1,13 @@
ruby '>= 2.6'
source 'https://rubygems.org'
# Middleman
gem 'middleman', '~> 4.4'
gem 'middleman-syntax', '~> 3.2'
gem 'middleman-autoprefixer', '~> 3.0'
gem 'middleman-sprockets', '~> 4.1'
gem 'rouge', '~> 3.21'
gem 'redcarpet', '~> 3.5.0'
gem 'nokogiri', '~> 1.13.3'
gem 'sass'
gem 'webrick'

View File

@ -0,0 +1,145 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
autoprefixer-rails (10.2.5.0)
execjs (< 2.8.0)
backports (3.21.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.9)
contracts (0.13.0)
dotenv (2.7.6)
erubis (2.7.0)
execjs (2.7.0)
fast_blank (1.0.1)
fastimage (2.2.5)
ffi (1.15.4)
haml (5.2.2)
temple (>= 0.8.0)
tilt
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashie (3.6.0)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
kramdown (2.3.1)
rexml
listen (3.0.8)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
memoist (0.16.2)
middleman (4.4.2)
coffee-script (~> 2.2)
haml (>= 4.0.5)
kramdown (>= 2.3.0)
middleman-cli (= 4.4.2)
middleman-core (= 4.4.2)
middleman-autoprefixer (3.0.0)
autoprefixer-rails (~> 10.0)
middleman-core (>= 4.0.0)
middleman-cli (4.4.2)
thor (>= 0.17.0, < 2.0)
middleman-core (4.4.2)
activesupport (>= 6.1, < 7.0)
addressable (~> 2.4)
backports (~> 3.6)
bundler (~> 2.0)
contracts (~> 0.13.0)
dotenv
erubis
execjs (~> 2.0)
fast_blank
fastimage (~> 2.0)
hamster (~> 3.0)
hashie (~> 3.4)
i18n (~> 1.6.0)
listen (~> 3.0.0)
memoist (~> 0.14)
padrino-helpers (~> 0.15.0)
parallel
rack (>= 1.4.5, < 3)
sassc (~> 2.0)
servolux
tilt (~> 2.0.9)
toml
uglifier (~> 3.0)
webrick
middleman-sprockets (4.1.1)
middleman-core (~> 4.0)
sprockets (>= 3.0)
middleman-syntax (3.2.0)
middleman-core (>= 3.2)
rouge (~> 3.2)
mini_portile2 (2.8.0)
minitest (5.14.4)
nokogiri (1.13.4)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
padrino-helpers (0.15.1)
i18n (>= 0.6.7, < 2)
padrino-support (= 0.15.1)
tilt (>= 1.4.1, < 3)
padrino-support (0.15.1)
parallel (1.21.0)
parslet (2.0.0)
public_suffix (4.0.6)
racc (1.6.0)
rack (2.2.3)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.5.1)
rexml (3.2.5)
rouge (3.28.0)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.4.0)
ffi (~> 1.9)
servolux (0.13.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
temple (0.8.2)
thor (1.1.0)
tilt (2.0.10)
toml (0.3.0)
parslet (>= 1.8.0, < 3.0.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
webrick (1.7.0)
zeitwerk (2.5.1)
PLATFORMS
ruby
DEPENDENCIES
middleman (~> 4.4)
middleman-autoprefixer (~> 3.0)
middleman-sprockets (~> 4.1)
middleman-syntax (~> 3.2)
nokogiri (~> 1.13.3)
redcarpet (~> 3.5.0)
rouge (~> 3.21)
sass
webrick
RUBY VERSION
ruby 2.7.2p137
BUNDLED WITH
2.2.22

63
docs/api/config.rb 100644
View File

@ -0,0 +1,63 @@
# Unique header generation
require './lib/unique_head.rb'
# Markdown
set :markdown_engine, :redcarpet
set :markdown,
fenced_code_blocks: true,
smartypants: true,
disable_indented_code_blocks: true,
prettify: true,
strikethrough: true,
tables: true,
with_toc_data: true,
no_intra_emphasis: true,
renderer: UniqueHeadCounter
# Assets
set :css_dir, 'stylesheets'
set :js_dir, 'javascripts'
set :images_dir, 'images'
set :fonts_dir, 'fonts'
# Activate the syntax highlighter
activate :syntax
ready do
require './lib/monokai_sublime_slate.rb'
require './lib/multilang.rb'
end
activate :sprockets
activate :autoprefixer do |config|
config.browsers = ['last 2 version', 'Firefox ESR']
config.cascade = false
config.inline = true
end
# Github pages require relative links
activate :relative_assets
set :relative_links, true
# Build Configuration
configure :build do
# We do want to hash woff and woff2 as there's a bug where woff2 will use
# woff asset hash which breaks things. Trying to use a combination of ignore and
# rewrite_ignore does not work as it conflicts weirdly with relative_assets. Disabling
# the .woff2 extension only does not work as .woff will still activate it so have to
# have both. See https://github.com/slatedocs/slate/issues/1171 for more details.
activate :asset_hash, :exts => app.config[:asset_extensions] - %w[.woff .woff2]
# If you're having trouble with Middleman hanging, commenting
# out the following two lines has been known to help
activate :minify_css
activate :minify_javascript
# activate :gzip
end
# Deploy Configuration
# If you want Middleman to listen on a different port, you can set that below
set :port, 4567
helpers do
require './lib/toc_data.rb'
end

View File

@ -0,0 +1,148 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z"
],
"attrs": [],
"isMulticolor": false,
"tags": [
"exclamation-circle"
],
"defaultCode": 61546,
"grid": 14
},
"attrs": [],
"properties": {
"id": 100,
"order": 4,
"prevSize": 28,
"code": 58880,
"name": "exclamation-sign",
"ligatures": ""
},
"setIdx": 0,
"iconIdx": 0
},
{
"icon": {
"paths": [
"M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z"
],
"attrs": [],
"isMulticolor": false,
"tags": [
"info-circle"
],
"defaultCode": 61530,
"grid": 14
},
"attrs": [],
"properties": {
"id": 85,
"order": 3,
"name": "info-sign",
"prevSize": 28,
"code": 58882
},
"setIdx": 0,
"iconIdx": 2
},
{
"icon": {
"paths": [
"M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z"
],
"attrs": [],
"isMulticolor": false,
"tags": [
"check-circle"
],
"defaultCode": 61528,
"grid": 14
},
"attrs": [],
"properties": {
"id": 83,
"order": 9,
"prevSize": 28,
"code": 58886,
"name": "ok-sign"
},
"setIdx": 0,
"iconIdx": 6
},
{
"icon": {
"paths": [
"M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z"
],
"width": 951,
"attrs": [],
"isMulticolor": false,
"tags": [
"search"
],
"defaultCode": 61442,
"grid": 14
},
"attrs": [],
"properties": {
"id": 2,
"order": 1,
"prevSize": 28,
"code": 58887,
"name": "icon-search"
},
"setIdx": 0,
"iconIdx": 7
}
],
"height": 1024,
"metadata": {
"name": "slate",
"license": "SIL OFL 1.1"
},
"preferences": {
"showGlyphs": true,
"showQuickUse": true,
"showQuickUse2": true,
"showSVGs": true,
"fontPref": {
"prefix": "icon-",
"metadata": {
"fontFamily": "slate",
"majorVersion": 1,
"minorVersion": 0,
"description": "Based on FontAwesome",
"license": "SIL OFL 1.1"
},
"metrics": {
"emSize": 1024,
"baseline": 6.25,
"whitespace": 50
},
"resetPoint": 58880,
"showSelector": false,
"selector": "class",
"classSelector": ".icon",
"showMetrics": false,
"showMetadata": true,
"showVersion": true,
"ie7": false
},
"imagePref": {
"prefix": "icon-",
"png": true,
"useClassSelector": true,
"color": 4473924,
"bgColor": 16777215
},
"historySize": 100,
"showCodes": true,
"gridSize": 16,
"showLiga": false
}
}

View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true
# this is based on https://github.com/rouge-ruby/rouge/blob/master/lib/rouge/themes/monokai_sublime.rb
# but without the added background, and changed styling for JSON keys to be soft_yellow instead of white
module Rouge
module Themes
class MonokaiSublimeSlate < CSSTheme
name 'monokai.sublime.slate'
palette :black => '#000000'
palette :bright_green => '#a6e22e'
palette :bright_pink => '#f92672'
palette :carmine => '#960050'
palette :dark => '#49483e'
palette :dark_grey => '#888888'
palette :dark_red => '#aa0000'
palette :dimgrey => '#75715e'
palette :emperor => '#555555'
palette :grey => '#999999'
palette :light_grey => '#aaaaaa'
palette :light_violet => '#ae81ff'
palette :soft_cyan => '#66d9ef'
palette :soft_yellow => '#e6db74'
palette :very_dark => '#1e0010'
palette :whitish => '#f8f8f2'
palette :orange => '#f6aa11'
palette :white => '#ffffff'
style Generic::Heading, :fg => :grey
style Literal::String::Regex, :fg => :orange
style Generic::Output, :fg => :dark_grey
style Generic::Prompt, :fg => :emperor
style Generic::Strong, :bold => false
style Generic::Subheading, :fg => :light_grey
style Name::Builtin, :fg => :orange
style Comment::Multiline,
Comment::Preproc,
Comment::Single,
Comment::Special,
Comment, :fg => :dimgrey
style Error,
Generic::Error,
Generic::Traceback, :fg => :carmine
style Generic::Deleted,
Generic::Inserted,
Generic::Emph, :fg => :dark
style Keyword::Constant,
Keyword::Declaration,
Keyword::Reserved,
Name::Constant,
Keyword::Type, :fg => :soft_cyan
style Literal::Number::Float,
Literal::Number::Hex,
Literal::Number::Integer::Long,
Literal::Number::Integer,
Literal::Number::Oct,
Literal::Number,
Literal::String::Char,
Literal::String::Escape,
Literal::String::Symbol, :fg => :light_violet
style Literal::String::Doc,
Literal::String::Double,
Literal::String::Backtick,
Literal::String::Heredoc,
Literal::String::Interpol,
Literal::String::Other,
Literal::String::Single,
Literal::String, :fg => :soft_yellow
style Name::Attribute,
Name::Class,
Name::Decorator,
Name::Exception,
Name::Function, :fg => :bright_green
style Name::Variable::Class,
Name::Namespace,
Name::Entity,
Name::Builtin::Pseudo,
Name::Variable::Global,
Name::Variable::Instance,
Name::Variable,
Text::Whitespace,
Text,
Name, :fg => :white
style Name::Label, :fg => :bright_pink
style Operator::Word,
Name::Tag,
Keyword,
Keyword::Namespace,
Keyword::Pseudo,
Operator, :fg => :bright_pink
end
end
end

View File

@ -0,0 +1,16 @@
module Multilang
def block_code(code, full_lang_name)
if full_lang_name
parts = full_lang_name.split('--')
rouge_lang_name = (parts) ? parts[0] : "" # just parts[0] here causes null ref exception when no language specified
super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match|
match + " tab-" + full_lang_name
end
else
super(code, full_lang_name)
end
end
end
require 'middleman-core/renderers/redcarpet'
Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang

View File

@ -0,0 +1,22 @@
# Nested unique header generation
require 'middleman-core/renderers/redcarpet'
class NestingUniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML
def initialize
super
@@headers_history = {} if !defined?(@@headers_history)
end
def header(text, header_level)
friendly_text = text.gsub(/<[^>]*>/,"").parameterize
@@headers_history[header_level] = text.parameterize
if header_level > 1
for i in (header_level - 1).downto(1)
friendly_text.prepend("#{@@headers_history[i]}-") if @@headers_history.key?(i)
end
end
return "<h#{header_level} id='#{friendly_text}'>#{text}</h#{header_level}>"
end
end

View File

@ -0,0 +1,31 @@
require 'nokogiri'
def toc_data(page_content)
html_doc = Nokogiri::HTML::DocumentFragment.parse(page_content)
# get a flat list of headers
headers = []
html_doc.css('h1, h2, h3').each do |header|
headers.push({
id: header.attribute('id').to_s,
content: header.children,
title: header.children.to_s.gsub(/<[^>]*>/, ''),
level: header.name[1].to_i,
children: []
})
end
[3,2].each do |header_level|
header_to_nest = nil
headers = headers.reject do |header|
if header[:level] == header_level
header_to_nest[:children].push header if header_to_nest
true
else
header_to_nest = header if header[:level] < header_level
false
end
end
end
headers
end

View File

@ -0,0 +1,24 @@
# Unique header generation
require 'middleman-core/renderers/redcarpet'
require 'digest'
class UniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML
def initialize
super
@head_count = {}
end
def header(text, header_level)
friendly_text = text.gsub(/<[^>]*>/,"").parameterize
if friendly_text.strip.length == 0
# Looks like parameterize removed the whole thing! It removes many unicode
# characters like Chinese and Russian. To get a unique URL, let's just
# URI escape the whole header
friendly_text = Digest::SHA1.hexdigest(text)[0,10]
end
@head_count[friendly_text] ||= 0
@head_count[friendly_text] += 1
if @head_count[friendly_text] > 1
friendly_text += "-#{@head_count[friendly_text]}"
end
return "<h#{header_level} id='#{friendly_text}'>#{text}</h#{header_level}>"
end
end

248
docs/api/slate.sh 100755
View File

@ -0,0 +1,248 @@
#!/usr/bin/env bash
set -o errexit #abort if any command fails
me=$(basename "$0")
help_message="\
Usage: $me [<options>] <command> [<command-options>]
Run commands related to the slate process.
Commands:
serve Run the middleman server process, useful for
development.
build Run the build process.
deploy Will build and deploy files to branch. Use
--no-build to only deploy.
Global Options:
-h, --help Show this help information.
-v, --verbose Increase verbosity. Useful for debugging.
Deploy options:
-e, --allow-empty Allow deployment of an empty directory.
-m, --message MESSAGE Specify the message used when committing on the
deploy branch.
-n, --no-hash Don't append the source commit's hash to the deploy
commit's message.
--no-build Do not build the source files.
"
run_serve() {
exec bundle exec middleman serve --watcher-force-polling
}
run_build() {
bundle exec middleman build --clean
}
parse_args() {
# Set args from a local environment file.
if [ -e ".env" ]; then
source .env
fi
command=
# Parse arg flags
# If something is exposed as an environment variable, set/overwrite it
# here. Otherwise, set/overwrite the internal variable instead.
while : ; do
if [[ $1 = "-h" || $1 = "--help" ]]; then
echo "$help_message"
exit 0
elif [[ $1 = "-v" || $1 = "--verbose" ]]; then
verbose=true
shift
elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then
allow_empty=true
shift
elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then
commit_message=$2
shift 2
elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then
GIT_DEPLOY_APPEND_HASH=false
shift
elif [[ $1 = "--no-build" ]]; then
no_build=true
shift
elif [[ $1 = "serve" || $1 = "build" || $1 = "deploy" ]]; then
if [ ! -z "${command}" ]; then
>&2 echo "You can only specify one command."
exit 1
fi
command=$1
shift
elif [ -z $1 ]; then
break
fi
done
if [ -z "${command}" ]; then
>&2 echo "Command not specified."
exit 1
fi
# Set internal option vars from the environment and arg flags. All internal
# vars should be declared here, with sane defaults if applicable.
# Source directory & target branch.
deploy_directory=build
deploy_branch=gh-pages
#if no user identity is already set in the current git environment, use this:
default_username=${GIT_DEPLOY_USERNAME:-deploy.sh}
default_email=${GIT_DEPLOY_EMAIL:-}
#repository to deploy to. must be readable and writable.
repo=origin
#append commit hash to the end of message by default
append_hash=${GIT_DEPLOY_APPEND_HASH:-true}
}
main() {
enable_expanded_output
if ! git diff --exit-code --quiet --cached; then
echo Aborting due to uncommitted changes in the index >&2
return 1
fi
commit_title=`git log -n 1 --format="%s" HEAD`
commit_hash=` git log -n 1 --format="%H" HEAD`
#default commit message uses last title if a custom one is not supplied
if [[ -z $commit_message ]]; then
commit_message="publish: $commit_title"
fi
#append hash to commit message unless no hash flag was found
if [ $append_hash = true ]; then
commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash"
fi
previous_branch=`git rev-parse --abbrev-ref HEAD`
if [ ! -d "$deploy_directory" ]; then
echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2
return 1
fi
# must use short form of flag in ls for compatibility with macOS and BSD
if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then
echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2
return 1
fi
if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then
# deploy_branch exists in $repo; make sure we have the latest version
disable_expanded_output
git fetch --force $repo $deploy_branch:$deploy_branch
enable_expanded_output
fi
# check if deploy_branch exists locally
if git show-ref --verify --quiet "refs/heads/$deploy_branch"
then incremental_deploy
else initial_deploy
fi
restore_head
}
initial_deploy() {
git --work-tree "$deploy_directory" checkout --orphan $deploy_branch
git --work-tree "$deploy_directory" add --all
commit+push
}
incremental_deploy() {
#make deploy_branch the current branch
git symbolic-ref HEAD refs/heads/$deploy_branch
#put the previously committed contents of deploy_branch into the index
git --work-tree "$deploy_directory" reset --mixed --quiet
git --work-tree "$deploy_directory" add --all
set +o errexit
diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$?
set -o errexit
case $diff in
0) echo No changes to files in $deploy_directory. Skipping commit.;;
1) commit+push;;
*)
echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2
return $diff
;;
esac
}
commit+push() {
set_user_id
git --work-tree "$deploy_directory" commit -m "$commit_message"
disable_expanded_output
#--quiet is important here to avoid outputting the repo URL, which may contain a secret token
git push --quiet $repo $deploy_branch
enable_expanded_output
}
#echo expanded commands as they are executed (for debugging)
enable_expanded_output() {
if [ $verbose ]; then
set -o xtrace
set +o verbose
fi
}
#this is used to avoid outputting the repo URL, which may contain a secret token
disable_expanded_output() {
if [ $verbose ]; then
set +o xtrace
set -o verbose
fi
}
set_user_id() {
if [[ -z `git config user.name` ]]; then
git config user.name "$default_username"
fi
if [[ -z `git config user.email` ]]; then
git config user.email "$default_email"
fi
}
restore_head() {
if [[ $previous_branch = "HEAD" ]]; then
#we weren't on any branch before, so just set HEAD back to the commit it was on
git update-ref --no-deref HEAD $commit_hash $deploy_branch
else
git symbolic-ref HEAD refs/heads/$previous_branch
fi
git reset --mixed
}
filter() {
sed -e "s|$repo|\$repo|g"
}
sanitize() {
"$@" 2> >(filter 1>&2) | filter
}
parse_args "$@"
if [ "${command}" = "serve" ]; then
run_serve
elif [[ "${command}" = "build" ]]; then
run_build
elif [[ ${command} = "deploy" ]]; then
if [[ ${no_build} != true ]]; then
run_build
fi
main "$@"
fi

Binary file not shown.

View File

@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="slate" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" d="" horiz-adv-x="512" />
<glyph unicode="&#xe600;" d="M438.857 877.714q119.429 0 220.286-58.857t159.714-159.714 58.857-220.286-58.857-220.286-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857zM512 165.143v108.571q0 8-5.143 13.429t-12.571 5.429h-109.714q-7.429 0-13.143-5.714t-5.714-13.143v-108.571q0-7.429 5.714-13.143t13.143-5.714h109.714q7.429 0 12.571 5.429t5.143 13.429zM510.857 361.714l10.286 354.857q0 6.857-5.714 10.286-5.714 4.571-13.714 4.571h-125.714q-8 0-13.714-4.571-5.714-3.429-5.714-10.286l9.714-354.857q0-5.714 5.714-10t13.714-4.286h105.714q8 0 13.429 4.286t6 10z" />
<glyph unicode="&#xe602;" d="M585.143 164.571v91.429q0 8-5.143 13.143t-13.143 5.143h-54.857v292.571q0 8-5.143 13.143t-13.143 5.143h-182.857q-8 0-13.143-5.143t-5.143-13.143v-91.429q0-8 5.143-13.143t13.143-5.143h54.857v-182.857h-54.857q-8 0-13.143-5.143t-5.143-13.143v-91.429q0-8 5.143-13.143t13.143-5.143h256q8 0 13.143 5.143t5.143 13.143zM512 676.571v91.429q0 8-5.143 13.143t-13.143 5.143h-109.714q-8 0-13.143-5.143t-5.143-13.143v-91.429q0-8 5.143-13.143t13.143-5.143h109.714q8 0 13.143 5.143t5.143 13.143zM877.714 438.857q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
<glyph unicode="&#xe606;" d="M733.714 531.428q0 16-10.286 26.286l-52 51.429q-10.857 10.857-25.714 10.857t-25.714-10.857l-233.143-232.571-129.143 129.143q-10.857 10.857-25.714 10.857t-25.714-10.857l-52-51.429q-10.286-10.286-10.286-26.286 0-15.429 10.286-25.714l206.857-206.857q10.857-10.857 25.714-10.857 15.429 0 26.286 10.857l310.286 310.286q10.286 10.286 10.286 25.714zM877.714 438.857q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
<glyph unicode="&#xe607;" d="M658.286 475.428q0 105.714-75.143 180.857t-180.857 75.143-180.857-75.143-75.143-180.857 75.143-180.857 180.857-75.143 180.857 75.143 75.143 180.857zM950.857 0q0-29.714-21.714-51.429t-51.429-21.714q-30.857 0-51.429 21.714l-196 195.429q-102.286-70.857-228-70.857-81.714 0-156.286 31.714t-128.571 85.714-85.714 128.571-31.714 156.286 31.714 156.286 85.714 128.571 128.571 85.714 156.286 31.714 156.286-31.714 128.571-85.714 85.714-128.571 31.714-156.286q0-125.714-70.857-228l196-196q21.143-21.143 21.143-51.429z" horiz-adv-x="951" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@ -0,0 +1,154 @@
# Git Repositories
<aside class="notice">
All routes in this section require authentication.
</aside>
Endpoints for interacting with the list of Git repositories stored on the
server.
## List repos
```shell
curl \
-H 'X-Api-Key: secret' \
https://example.com/api/repos?offset=10&limit=20
```
> JSON output format
```json
{
"message": "",
"data": [
{
"id": 1,
"url": "https://aur.archlinux.org/discord-ptb.git",
"branch": "master",
"repo": "bur",
"schedule": "",
"arch": [
{
"id": 1,
"repo_id": 1,
"value": "x86_64"
}
]
}
]
}
```
Retrieve a list of Git repositories.
### HTTP Request
`GET /api/repos`
### Query Parameters
Parameter | Description
--------- | -----------
limit | Maximum amount of results to return.
offset | Offset of results.
repo | Limit results to repositories that publish to the given repo.
## Get a repo
```shell
curl \
-H 'X-Api-Key: secret' \
https://example.com/api/repos/15
```
> JSON output format
```json
{
"message": "",
"data": {
"id": 1,
"url": "https://aur.archlinux.org/discord-ptb.git",
"branch": "master",
"repo": "bur",
"schedule": "0 3",
"arch": [
{
"id": 1,
"repo_id": 1,
"value": "x86_64"
}
]
}
}
```
Get info about a specific Git repository.
### HTTP Request
`GET /api/repos/:id`
### URL Parameters
Parameter | Description
--------- | -----------
id | ID of requested repo
## Create a new repo
Create a new Git repository with the given data.
### HTTP Request
`POST /api/repos`
### Query Parameters
Parameter | Description
--------- | -----------
url | URL of the Git repository.
branch | Branch of the Git repository.
repo | Vieter repository to publish built packages to.
schedule | Cron build schedule (syntax explained [here](https://rustybever.be/docs/vieter/usage/builds/schedule/))
arch | Comma-separated list of architectures to build package on.
## Modify a repo
Modify the data of an existing Git repository.
### HTTP Request
`PATCH /api/repos/:id`
### URL Parameters
Parameter | Description
--------- | -----------
id | ID of requested repo
### Query Parameters
Parameter | Description
--------- | -----------
url | URL of the Git repository.
branch | Branch of the Git repository.
repo | Vieter repository to publish built packages to.
schedule | Cron build schedule
arch | Comma-separated list of architectures to build package on.
## Remove a repo
Remove a Git repository from the server.
### HTTP Request
`DELETE /api/repos/:id`
### URL Parameters
Parameter | Description
--------- | -----------
id | ID of repo to remove

View File

@ -0,0 +1,140 @@
# Build Logs
<aside class="notice">
All routes in this section require authentication.
</aside>
Endpoints for interacting with stored build logs.
## List logs
```shell
curl \
-H 'X-Api-Key: secret' \
https://example.com/api/logs?offset=10&limit=20
```
> JSON output format
```json
{
"message": "",
"data": [
{
"id": 1,
"repo_id": 3,
"start_time": 1652008554,
"end_time": 1652008559,
"arch": "x86_64",
"exit_code": 0
}
]
}
```
Retrieve a list of build logs.
### HTTP Request
`GET /api/logs`
### Query Parameters
Parameter | Description
--------- | -----------
limit | Maximum amount of results to return.
offset | Offset of results.
repo | Only return builds published to this repository.
before | Only return logs started before this time (UTC epoch)
after | Only return logs started after this time (UTC epoch)
arch | Only return logs built on this architecture
exit_codes | Comma-separated list of exit codes to limit result to; using `!` as a prefix makes it exclude that value. For example, `1,2` only returns logs with status code 1 or 2, while `!1,!2` returns those that don't have 1 or 2 as the result.
## Get build log
```shell
curl \
-H 'X-Api-Key: secret' \
https://example.com/api/logs/15
```
> JSON output format
```json
{
"message": "",
"data": {
"id": 1,
"repo_id": 3,
"start_time": 1652008554,
"end_time": 1652008559,
"arch": "x86_64",
"exit_code": 0
}
}
```
Retrieve info about a specific build log.
### HTTP Request
`GET /api/logs/:id`
### URL Parameters
Parameter | Description
--------- | -----------
id | ID of requested log
## Get log contents
```shell
curl \
-H 'X-Api-Key: secret' \
https://example.com/api/logs/15/content
```
Retrieve the contents of a build log. The response is the build log in
plaintext.
### HTTP Request
`GET /api/logs/:id/content`
### URL Parameters
Parameter | Description
--------- | -----------
id | ID of requested log
## Publish build log
<aside class="warning">
You should probably not use this endpoint, as it's used by the build system to
publish its logs.
</aside>
Publish a new build log to the server.
### HTTP Request
`POST /api/logs`
### Query parameters
Parameter | Description
--------- | -----------
id | ID of requested log
startTime | Start time of the build (UTC epoch)
endTime | End time of the build (UTC epoch)
arch | Architecture on which the build was done
exitCode | Exit code of the build container
### Request body
Plaintext contents of the build log.

View File

@ -0,0 +1,95 @@
# Repository
Besides providing a RESTful API, the Vieter server is also a Pacman-compatible
repository server. This section describes the various routes that make this
possible.
## Get a package archive or database file
```shell
curl -L https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst
```
This endpoint is really the entire repository. It serves both the package
archives & the database files for a specific arch-repo. It has three different
behaviors, depending on `filename`:
* If the file extension is one of `.db`, `.files`, `.db.tar.gz` or
`.files.tar.gz`, it tries to serve the requested database file.
* If the filename contains `.pkg`, it serves the package file.
* Otherwise, it assumes `filename` is the name & version of a package inside
the repository (e.g. `vieter-0.3.0_alpha.2-1`) & serves that package's `desc`
file from inside the database archive.
<aside class="notice">
The final option might sound a little strange, but it's used by the build
system to determine whether a package needs to be rebuilt.
</aside>
### HTTP Request
`GET /:repo/:arch/:filename`
### URL Parameters
Parameter | Description
--------- | -----------
repo | Repository containing the package
arch | Arch-repo containing the package
filename | actual filename to request
## Check whether file exists
```shell
curl -L https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst
```
The above request can also be performed as a HEAD request. The behavior is the
same, except no data is returned besides an error 404 if the file doesn't exist
& an error 200 otherwise.
### HTTP Request
`GET /:repo/:arch/:filename`
### URL Parameters
Parameter | Description
--------- | -----------
repo | Repository containing the package
arch | Arch-repo containing the package
filename | actual filename to request
## Publish package
<aside class="notice">
This endpoint requests authentication.
</aside>
```shell
curl \
-H 'X-Api-Key: secret' \
-XPOST \
-T tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst \
https://example.com/some-repo/publish
```
This endpoint allows you to publish a new package archive to a given repo.
If the package's architecture is not `any`, it is added to that specific
arch-repo. Otherwise, it is added to the configured default architecture & any
other already present arch-repos.
### HTTP Request
`POST /:repo/publish`
### URL Parameters
Parameter | Description
--------- | -----------
repo | Repository to publish package to

View File

@ -0,0 +1,39 @@
---
title: API Reference
language_tabs: # must be one of https://git.io/vQNgJ
- shell: cURL
toc_footers:
- <a href='https://github.com/slatedocs/slate'>Documentation Powered by Slate</a>
includes:
- repository
- git
- logs
search: true
code_clipboard: true
meta:
- name: description
content: Documentation for the Vieter API
---
# Introduction
Welcome to the Vieter API documentation! Here, you can find everything related
to interacting with Vieter's HTTP API.
# Authentication
```shell
curl -H 'X-Api-Key: secret' https://example.com/api/some/path
```
> Don't forget to replace `secret` with your Vieter instance's secret.
Authentication is done by passing the HTTP header `X-Api-Key: secret` along
with each request, where `secret` is replaced with your Vieter server's
configured secret.

View File

@ -0,0 +1,2 @@
//= require ./all_nosearch
//= require ./app/_search

View File

@ -0,0 +1,27 @@
//= require ./lib/_energize
//= require ./app/_copy
//= require ./app/_toc
//= require ./app/_lang
function adjustLanguageSelectorWidth() {
const elem = $('.dark-box > .lang-selector');
elem.width(elem.parent().width());
}
$(function() {
loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10);
setupLanguages($('body').data('languages'));
$('.content').imagesLoaded( function() {
window.recacheHeights();
window.refreshToc();
});
$(window).resize(function() {
adjustLanguageSelectorWidth();
});
adjustLanguageSelectorWidth();
});
window.onpopstate = function() {
activateLanguage(getLanguageFromQueryString());
};

View File

@ -0,0 +1,15 @@
function copyToClipboard(container) {
const el = document.createElement('textarea');
el.value = container.textContent.replace(/\n$/, '');
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
function setupCodeCopy() {
$('pre.highlight').prepend('<div class="copy-clipboard"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Copy to Clipboard</title><path d="M18 6v-6h-18v18h6v6h18v-18h-6zm-12 10h-4v-14h14v4h-10v10zm16 6h-14v-14h14v14z"></path></svg></div>');
$('.copy-clipboard').on('click', function() {
copyToClipboard(this.parentNode.children[1]);
});
}

View File

@ -0,0 +1,171 @@
//= require ../lib/_jquery
/*
Copyright 2008-2013 Concur Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/
;(function () {
'use strict';
var languages = [];
window.setupLanguages = setupLanguages;
window.activateLanguage = activateLanguage;
window.getLanguageFromQueryString = getLanguageFromQueryString;
function activateLanguage(language) {
if (!language) return;
if (language === "") return;
$(".lang-selector a").removeClass('active');
$(".lang-selector a[data-language-name='" + language + "']").addClass('active');
for (var i=0; i < languages.length; i++) {
$(".highlight.tab-" + languages[i]).hide();
$(".lang-specific." + languages[i]).hide();
}
$(".highlight.tab-" + language).show();
$(".lang-specific." + language).show();
window.recacheHeights();
// scroll to the new location of the position
if ($(window.location.hash).get(0)) {
$(window.location.hash).get(0).scrollIntoView(true);
}
}
// parseURL and stringifyURL are from https://github.com/sindresorhus/query-string
// MIT licensed
// https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license
function parseURL(str) {
if (typeof str !== 'string') {
return {};
}
str = str.trim().replace(/^(\?|#|&)/, '');
if (!str) {
return {};
}
return str.split('&').reduce(function (ret, param) {
var parts = param.replace(/\+/g, ' ').split('=');
var key = parts[0];
var val = parts[1];
key = decodeURIComponent(key);
// missing `=` should be `null`:
// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
val = val === undefined ? null : decodeURIComponent(val);
if (!ret.hasOwnProperty(key)) {
ret[key] = val;
} else if (Array.isArray(ret[key])) {
ret[key].push(val);
} else {
ret[key] = [ret[key], val];
}
return ret;
}, {});
};
function stringifyURL(obj) {
return obj ? Object.keys(obj).sort().map(function (key) {
var val = obj[key];
if (Array.isArray(val)) {
return val.sort().map(function (val2) {
return encodeURIComponent(key) + '=' + encodeURIComponent(val2);
}).join('&');
}
return encodeURIComponent(key) + '=' + encodeURIComponent(val);
}).join('&') : '';
};
// gets the language set in the query string
function getLanguageFromQueryString() {
if (location.search.length >= 1) {
var language = parseURL(location.search).language;
if (language) {
return language;
} else if (jQuery.inArray(location.search.substr(1), languages) != -1) {
return location.search.substr(1);
}
}
return false;
}
// returns a new query string with the new language in it
function generateNewQueryString(language) {
var url = parseURL(location.search);
if (url.language) {
url.language = language;
return stringifyURL(url);
}
return language;
}
// if a button is clicked, add the state to the history
function pushURL(language) {
if (!history) { return; }
var hash = window.location.hash;
if (hash) {
hash = hash.replace(/^#+/, '');
}
history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash);
// save language as next default
if (localStorage) {
localStorage.setItem("language", language);
}
}
function setupLanguages(l) {
var defaultLanguage = null;
if (localStorage) {
defaultLanguage = localStorage.getItem("language");
}
languages = l;
var presetLanguage = getLanguageFromQueryString();
if (presetLanguage) {
// the language is in the URL, so use that language!
activateLanguage(presetLanguage);
if (localStorage) {
localStorage.setItem("language", presetLanguage);
}
} else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) {
// the language was the last selected one saved in localstorage, so use that language!
activateLanguage(defaultLanguage);
} else {
// no language selected, so use the default
activateLanguage(languages[0]);
}
}
// if we click on a language tab, activate that language
$(function() {
$(".lang-selector a").on("click", function() {
var language = $(this).data("language-name");
pushURL(language);
activateLanguage(language);
return false;
});
});
})();

View File

@ -0,0 +1,102 @@
//= require ../lib/_lunr
//= require ../lib/_jquery
//= require ../lib/_jquery.highlight
;(function () {
'use strict';
var content, searchResults;
var highlightOpts = { element: 'span', className: 'search-highlight' };
var searchDelay = 0;
var timeoutHandle = 0;
var index;
function populate() {
index = lunr(function(){
this.ref('id');
this.field('title', { boost: 10 });
this.field('body');
this.pipeline.add(lunr.trimmer, lunr.stopWordFilter);
var lunrConfig = this;
$('h1, h2').each(function() {
var title = $(this);
var body = title.nextUntil('h1, h2');
lunrConfig.add({
id: title.prop('id'),
title: title.text(),
body: body.text()
});
});
});
determineSearchDelay();
}
$(populate);
$(bind);
function determineSearchDelay() {
if (index.tokenSet.toArray().length>5000) {
searchDelay = 300;
}
}
function bind() {
content = $('.content');
searchResults = $('.search-results');
$('#input-search').on('keyup',function(e) {
var wait = function() {
return function(executingFunction, waitTime){
clearTimeout(timeoutHandle);
timeoutHandle = setTimeout(executingFunction, waitTime);
};
}();
wait(function(){
search(e);
}, searchDelay);
});
}
function search(event) {
var searchInput = $('#input-search')[0];
unhighlight();
searchResults.addClass('visible');
// ESC clears the field
if (event.keyCode === 27) searchInput.value = '';
if (searchInput.value) {
var results = index.search(searchInput.value).filter(function(r) {
return r.score > 0.0001;
});
if (results.length) {
searchResults.empty();
$.each(results, function (index, result) {
var elem = document.getElementById(result.ref);
searchResults.append("<li><a href='#" + result.ref + "'>" + $(elem).text() + "</a></li>");
});
highlight.call(searchInput);
} else {
searchResults.html('<li></li>');
$('.search-results li').text('No Results Found for "' + searchInput.value + '"');
}
} else {
unhighlight();
searchResults.removeClass('visible');
}
}
function highlight() {
if (this.value) content.highlight(this.value, highlightOpts);
}
function unhighlight() {
content.unhighlight(highlightOpts);
}
})();

View File

@ -0,0 +1,122 @@
//= require ../lib/_jquery
//= require ../lib/_imagesloaded.min
;(function () {
'use strict';
var htmlPattern = /<[^>]*>/g;
var loaded = false;
var debounce = function(func, waitTime) {
var timeout = false;
return function() {
if (timeout === false) {
setTimeout(function() {
func();
timeout = false;
}, waitTime);
timeout = true;
}
};
};
var closeToc = function() {
$(".toc-wrapper").removeClass('open');
$("#nav-button").removeClass('open');
};
function loadToc($toc, tocLinkSelector, tocListSelector, scrollOffset) {
var headerHeights = {};
var pageHeight = 0;
var windowHeight = 0;
var originalTitle = document.title;
var recacheHeights = function() {
headerHeights = {};
pageHeight = $(document).height();
windowHeight = $(window).height();
$toc.find(tocLinkSelector).each(function() {
var targetId = $(this).attr('href');
if (targetId[0] === "#") {
headerHeights[targetId] = $("#" + $.escapeSelector(targetId.substring(1))).offset().top;
}
});
};
var refreshToc = function() {
var currentTop = $(document).scrollTop() + scrollOffset;
if (currentTop + windowHeight >= pageHeight) {
// at bottom of page, so just select last header by making currentTop very large
// this fixes the problem where the last header won't ever show as active if its content
// is shorter than the window height
currentTop = pageHeight + 1000;
}
var best = null;
for (var name in headerHeights) {
if ((headerHeights[name] < currentTop && headerHeights[name] > headerHeights[best]) || best === null) {
best = name;
}
}
// Catch the initial load case
if (currentTop == scrollOffset && !loaded) {
best = window.location.hash;
loaded = true;
}
var $best = $toc.find("[href='" + best + "']").first();
if (!$best.hasClass("active")) {
// .active is applied to the ToC link we're currently on, and its parent <ul>s selected by tocListSelector
// .active-expanded is applied to the ToC links that are parents of this one
$toc.find(".active").removeClass("active");
$toc.find(".active-parent").removeClass("active-parent");
$best.addClass("active");
$best.parents(tocListSelector).addClass("active").siblings(tocLinkSelector).addClass('active-parent');
$best.siblings(tocListSelector).addClass("active");
$toc.find(tocListSelector).filter(":not(.active)").slideUp(150);
$toc.find(tocListSelector).filter(".active").slideDown(150);
if (window.history.replaceState) {
window.history.replaceState(null, "", best);
}
var thisTitle = $best.data("title");
if (thisTitle !== undefined && thisTitle.length > 0) {
document.title = thisTitle.replace(htmlPattern, "") + " " + originalTitle;
} else {
document.title = originalTitle;
}
}
};
var makeToc = function() {
recacheHeights();
refreshToc();
$("#nav-button").click(function() {
$(".toc-wrapper").toggleClass('open');
$("#nav-button").toggleClass('open');
return false;
});
$(".page-wrapper").click(closeToc);
$(".toc-link").click(closeToc);
// reload immediately after scrolling on toc click
$toc.find(tocLinkSelector).click(function() {
setTimeout(function() {
refreshToc();
}, 0);
});
$(window).scroll(debounce(refreshToc, 200));
$(window).resize(debounce(recacheHeights, 200));
};
makeToc();
window.recacheHeights = recacheHeights;
window.refreshToc = refreshToc;
}
window.loadToc = loadToc;
})();

View File

@ -0,0 +1,169 @@
/**
* energize.js v0.1.0
*
* Speeds up click events on mobile devices.
* https://github.com/davidcalhoun/energize.js
*/
(function() { // Sandbox
/**
* Don't add to non-touch devices, which don't need to be sped up
*/
if(!('ontouchstart' in window)) return;
var lastClick = {},
isThresholdReached, touchstart, touchmove, touchend,
click, closest;
/**
* isThresholdReached
*
* Compare touchstart with touchend xy coordinates,
* and only fire simulated click event if the coordinates
* are nearby. (don't want clicking to be confused with a swipe)
*/
isThresholdReached = function(startXY, xy) {
return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5;
};
/**
* touchstart
*
* Save xy coordinates when the user starts touching the screen
*/
touchstart = function(e) {
this.startXY = [e.touches[0].clientX, e.touches[0].clientY];
this.threshold = false;
};
/**
* touchmove
*
* Check if the user is scrolling past the threshold.
* Have to check here because touchend will not always fire
* on some tested devices (Kindle Fire?)
*/
touchmove = function(e) {
// NOOP if the threshold has already been reached
if(this.threshold) return false;
this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]);
};
/**
* touchend
*
* If the user didn't scroll past the threshold between
* touchstart and touchend, fire a simulated click.
*
* (This will fire before a native click)
*/
touchend = function(e) {
// Don't fire a click if the user scrolled past the threshold
if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) {
return;
}
/**
* Create and fire a click event on the target element
* https://developer.mozilla.org/en/DOM/event.initMouseEvent
*/
var touch = e.changedTouches[0],
evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
evt.simulated = true; // distinguish from a normal (nonsimulated) click
e.target.dispatchEvent(evt);
};
/**
* click
*
* Because we've already fired a click event in touchend,
* we need to listed for all native click events here
* and suppress them as necessary.
*/
click = function(e) {
/**
* Prevent ghost clicks by only allowing clicks we created
* in the click event we fired (look for e.simulated)
*/
var time = Date.now(),
timeDiff = time - lastClick.time,
x = e.clientX,
y = e.clientY,
xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)],
target = closest(e.target, 'A') || e.target, // needed for standalone apps
nodeName = target.nodeName,
isLink = nodeName === 'A',
standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href");
lastClick.time = time;
lastClick.x = x;
lastClick.y = y;
/**
* Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire),
* so we have to add more logic to determine the time of the last click. Not perfect...
*
* Older, simpler check: if((!e.simulated) || standAlone)
*/
if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) {
e.preventDefault();
e.stopPropagation();
if(!standAlone) return false;
}
/**
* Special logic for standalone web apps
* See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window
*/
if(standAlone) {
window.location = target.getAttribute("href");
}
/**
* Add an energize-focus class to the targeted link (mimics :focus behavior)
* TODO: test and/or remove? Does this work?
*/
if(!target || !target.classList) return;
target.classList.add("energize-focus");
window.setTimeout(function(){
target.classList.remove("energize-focus");
}, 150);
};
/**
* closest
* @param {HTMLElement} node current node to start searching from.
* @param {string} tagName the (uppercase) name of the tag you're looking for.
*
* Find the closest ancestor tag of a given node.
*
* Starts at node and goes up the DOM tree looking for a
* matching nodeName, continuing until hitting document.body
*/
closest = function(node, tagName){
var curNode = node;
while(curNode !== document.body) { // go up the dom until we find the tag we're after
if(!curNode || curNode.nodeName === tagName) { return curNode; } // found
curNode = curNode.parentNode; // not found, so keep going up
}
return null; // not found
};
/**
* Add all delegated event listeners
*
* All the events we care about bubble up to document,
* so we can take advantage of event delegation.
*
* Note: no need to wait for DOMContentLoaded here
*/
document.addEventListener('touchstart', touchstart, false);
document.addEventListener('touchmove', touchmove, false);
document.addEventListener('touchend', touchend, false);
document.addEventListener('click', click, true); // TODO: why does this use capture?
})();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,108 @@
/*
* jQuery Highlight plugin
*
* Based on highlight v3 by Johann Burkard
* http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
*
* Code a little bit refactored and cleaned (in my humble opinion).
* Most important changes:
* - has an option to highlight only entire words (wordsOnly - false by default),
* - has an option to be case sensitive (caseSensitive - false by default)
* - highlight element tag and class names can be specified in options
*
* Usage:
* // wrap every occurrance of text 'lorem' in content
* // with <span class='highlight'> (default options)
* $('#content').highlight('lorem');
*
* // search for and highlight more terms at once
* // so you can save some time on traversing DOM
* $('#content').highlight(['lorem', 'ipsum']);
* $('#content').highlight('lorem ipsum');
*
* // search only for entire word 'lorem'
* $('#content').highlight('lorem', { wordsOnly: true });
*
* // don't ignore case during search of term 'lorem'
* $('#content').highlight('lorem', { caseSensitive: true });
*
* // wrap every occurrance of term 'ipsum' in content
* // with <em class='important'>
* $('#content').highlight('ipsum', { element: 'em', className: 'important' });
*
* // remove default highlight
* $('#content').unhighlight();
*
* // remove custom highlight
* $('#content').unhighlight({ element: 'em', className: 'important' });
*
*
* Copyright (c) 2009 Bartek Szopka
*
* Licensed under MIT license.
*
*/
jQuery.extend({
highlight: function (node, re, nodeName, className) {
if (node.nodeType === 3) {
var match = node.data.match(re);
if (match) {
var highlight = document.createElement(nodeName || 'span');
highlight.className = className || 'highlight';
var wordNode = node.splitText(match.index);
wordNode.splitText(match[0].length);
var wordClone = wordNode.cloneNode(true);
highlight.appendChild(wordClone);
wordNode.parentNode.replaceChild(highlight, wordNode);
return 1; //skip added node in parent
}
} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
!(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted
for (var i = 0; i < node.childNodes.length; i++) {
i += jQuery.highlight(node.childNodes[i], re, nodeName, className);
}
}
return 0;
}
});
jQuery.fn.unhighlight = function (options) {
var settings = { className: 'highlight', element: 'span' };
jQuery.extend(settings, options);
return this.find(settings.element + "." + settings.className).each(function () {
var parent = this.parentNode;
parent.replaceChild(this.firstChild, this);
parent.normalize();
}).end();
};
jQuery.fn.highlight = function (words, options) {
var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false };
jQuery.extend(settings, options);
if (words.constructor === String) {
words = [words];
}
words = jQuery.grep(words, function(word, i){
return word != '';
});
words = jQuery.map(words, function(word, i) {
return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
});
if (words.length == 0) { return this; };
var flag = settings.caseSensitive ? "" : "i";
var pattern = "(" + words.join("|") + ")";
if (settings.wordsOnly) {
pattern = "\\b" + pattern + "\\b";
}
var re = new RegExp(pattern, flag);
return this.each(function () {
jQuery.highlight(this, re, settings.element, settings.className);
});
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,137 @@
<%#
Copyright 2008-2013 Concur Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
%>
<% language_tabs = current_page.data.language_tabs || [] %>
<% page_content = yield %>
<%
if current_page.data.includes
current_page.data.includes.each do |include|
page_content += partial("includes/#{include}")
end
end
%>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<% if current_page.data.key?('meta') %>
<% current_page.data.meta.each do |meta| %>
<meta
<% meta.each do |key, value| %>
<%= "#{key}=\"#{value}\"" %>
<% end %>
>
<% end %>
<% end %>
<title><%= current_page.data.title || "API Documentation" %></title>
<style media="screen">
<%= Rouge::Themes::MonokaiSublimeSlate.render(:scope => '.highlight') %>
</style>
<style media="print">
* {
transition:none!important;
}
<%= Rouge::Themes::Base16::Solarized.render(:scope => '.highlight') %>
</style>
<%= stylesheet_link_tag :screen, media: :screen %>
<%= stylesheet_link_tag :print, media: :print %>
<% if current_page.data.search %>
<%= javascript_include_tag "all" %>
<% else %>
<%= javascript_include_tag "all_nosearch" %>
<% end %>
<% if current_page.data.code_clipboard %>
<script>
$(function() { setupCodeCopy(); });
</script>
<% end %>
</head>
<body class="<%= page_classes %>" data-languages="<%=h language_tabs.map{ |lang| lang.is_a?(Hash) ? lang.keys.first : lang }.to_json %>">
<a href="#" id="nav-button">
<span>
NAV
<%= image_tag('navbar.png') %>
</span>
</a>
<div class="toc-wrapper">
<%= image_tag "logo.png", class: 'logo' %>
<% if language_tabs.any? %>
<div class="lang-selector">
<% language_tabs.each do |lang| %>
<% if lang.is_a? Hash %>
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
<% else %>
<a href="#" data-language-name="<%= lang %>"><%= lang %></a>
<% end %>
<% end %>
</div>
<% end %>
<% if current_page.data.search %>
<div class="search">
<input type="text" class="search" id="input-search" placeholder="Search">
</div>
<ul class="search-results"></ul>
<% end %>
<ul id="toc" class="toc-list-h1">
<% toc_data(page_content).each do |h1| %>
<li>
<a href="#<%= h1[:id] %>" class="toc-h1 toc-link" data-title="<%= h1[:title] %>"><%= h1[:content] %></a>
<% if h1[:children].length > 0 %>
<ul class="toc-list-h2">
<% h1[:children].each do |h2| %>
<li>
<a href="#<%= h2[:id] %>" class="toc-h2 toc-link" data-title="<%= h2[:title] %>"><%= h2[:content] %></a>
</li>
<% end %>
</ul>
<% end %>
</li>
<% end %>
</ul>
<% if current_page.data.toc_footers %>
<ul class="toc-footer">
<% current_page.data.toc_footers.each do |footer| %>
<li><%= footer %></li>
<% end %>
</ul>
<% end %>
</div>
<div class="page-wrapper">
<div class="dark-box"></div>
<div class="content">
<%= page_content %>
</div>
<div class="dark-box">
<% if language_tabs.any? %>
<div class="lang-selector">
<% language_tabs.each do |lang| %>
<% if lang.is_a? Hash %>
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
<% else %>
<a href="#" data-language-name="<%= lang %>"><%= lang %></a>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,38 @@
@font-face {
font-family: 'slate';
src:font-url('slate.eot?-syv14m');
src:font-url('slate.eot?#iefix-syv14m') format('embedded-opentype'),
font-url('slate.woff2?-syv14m') format('woff2'),
font-url('slate.woff?-syv14m') format('woff'),
font-url('slate.ttf?-syv14m') format('truetype'),
font-url('slate.svg?-syv14m#slate') format('svg');
font-weight: normal;
font-style: normal;
}
%icon {
font-family: 'slate';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
}
%icon-exclamation-sign {
@extend %icon;
content: "\e600";
}
%icon-info-sign {
@extend %icon;
content: "\e602";
}
%icon-ok-sign {
@extend %icon;
content: "\e606";
}
%icon-search {
@extend %icon;
content: "\e607";
}

View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@ -0,0 +1,140 @@
////////////////////////////////////////////////////////////////////////////////
// RTL Styles Variables
////////////////////////////////////////////////////////////////////////////////
$default: auto;
////////////////////////////////////////////////////////////////////////////////
// TABLE OF CONTENTS
////////////////////////////////////////////////////////////////////////////////
#toc>ul>li>a>span {
float: left;
}
.toc-wrapper {
transition: right 0.3s ease-in-out !important;
left: $default !important;
#{right}: 0;
}
.toc-h2 {
padding-#{right}: $nav-padding + $nav-indent;
}
#nav-button {
#{right}: 0;
transition: right 0.3s ease-in-out;
&.open {
right: $nav-width
}
}
////////////////////////////////////////////////////////////////////////////////
// PAGE LAYOUT AND CODE SAMPLE BACKGROUND
////////////////////////////////////////////////////////////////////////////////
.page-wrapper {
margin-#{left}: $default !important;
margin-#{right}: $nav-width;
.dark-box {
#{right}: $default;
#{left}: 0;
}
}
.lang-selector {
width: $default !important;
a {
float: right;
}
}
////////////////////////////////////////////////////////////////////////////////
// CODE SAMPLE STYLES
////////////////////////////////////////////////////////////////////////////////
.content {
&>h1,
&>h2,
&>h3,
&>h4,
&>h5,
&>h6,
&>p,
&>table,
&>ul,
&>ol,
&>aside,
&>dl {
margin-#{left}: $examples-width;
margin-#{right}: $default !important;
}
&>ul,
&>ol {
padding-#{right}: $main-padding + 15px;
}
table {
th,
td {
text-align: right;
}
}
dd {
margin-#{right}: 15px;
}
aside {
aside:before {
padding-#{left}: 0.5em;
}
.search-highlight {
background: linear-gradient(to top right, #F7E633 0%, #F1D32F 100%);
}
}
pre,
blockquote {
float: left !important;
clear: left !important;
}
}
////////////////////////////////////////////////////////////////////////////////
// TYPOGRAPHY
////////////////////////////////////////////////////////////////////////////////
h1,
h2,
h3,
h4,
h5,
h6,
p,
aside {
text-align: right;
direction: rtl;
}
.toc-wrapper {
text-align: right;
direction: rtl;
font-weight: 100 !important;
}
////////////////////////////////////////////////////////////////////////////////
// RESPONSIVE DESIGN
////////////////////////////////////////////////////////////////////////////////
@media (max-width: $tablet-width) {
.toc-wrapper {
#{right}: -$nav-width;
&.open {
#{right}: 0;
}
}
.page-wrapper {
margin-#{right}: 0;
}
}
@media (max-width: $phone-width) {
%left-col {
margin-#{left}: 0;
}
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2008-2013 Concur Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/
////////////////////////////////////////////////////////////////////////////////
// CUSTOMIZE SLATE
////////////////////////////////////////////////////////////////////////////////
// Use these settings to help adjust the appearance of Slate
// BACKGROUND COLORS
////////////////////
$nav-bg: #2E3336 !default;
$examples-bg: #2E3336 !default;
$code-bg: #1E2224 !default;
$code-annotation-bg: #191D1F !default;
$nav-subitem-bg: #1E2224 !default;
$nav-active-bg: #0F75D4 !default;
$nav-active-parent-bg: #1E2224 !default; // parent links of the current section
$lang-select-border: #000 !default;
$lang-select-bg: #1E2224 !default;
$lang-select-active-bg: $examples-bg !default; // feel free to change this to blue or something
$lang-select-pressed-bg: #111 !default; // color of language tab bg when mouse is pressed
$main-bg: #F3F7F9 !default;
$aside-notice-bg: #8fbcd4 !default;
$aside-warning-bg: #c97a7e !default;
$aside-success-bg: #6ac174 !default;
$search-notice-bg: #c97a7e !default;
// TEXT COLORS
////////////////////
$main-text: #333 !default; // main content text color
$nav-text: #fff !default;
$nav-active-text: #fff !default;
$nav-active-parent-text: #fff !default; // parent links of the current section
$lang-select-text: #fff !default; // color of unselected language tab text
$lang-select-active-text: #fff !default; // color of selected language tab text
$lang-select-pressed-text: #fff !default; // color of language tab text when mouse is pressed
// SIZES
////////////////////
$nav-width: 230px !default; // width of the navbar
$examples-width: 50% !default; // portion of the screen taken up by code examples
$logo-margin: 0px !default; // margin below logo
$main-padding: 28px !default; // padding to left and right of content & examples
$nav-padding: 15px !default; // padding to left and right of navbar
$nav-v-padding: 10px !default; // padding used vertically around search boxes and results
$nav-indent: 10px !default; // extra padding for ToC subitems
$code-annotation-padding: 13px !default; // padding inside code annotations
$h1-margin-bottom: 21px !default; // padding under the largest header tags
$tablet-width: 930px !default; // min width before reverting to tablet size
$phone-width: $tablet-width - $nav-width !default; // min width before reverting to mobile size
// FONTS
////////////////////
%default-font {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
}
%header-font {
@extend %default-font;
font-weight: bold;
}
%code-font {
font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
font-size: 12px;
line-height: 1.5;
}
// OTHER
////////////////////
$nav-footer-border-color: #666 !default;
$search-box-border-color: #666 !default;
////////////////////////////////////////////////////////////////////////////////
// INTERNAL
////////////////////////////////////////////////////////////////////////////////
// These settings are probably best left alone.
%break-words {
word-break: break-all;
hyphens: auto;
}

View File

@ -0,0 +1,153 @@
@charset "utf-8";
@import 'normalize';
@import 'variables';
@import 'icon-font';
/*
Copyright 2008-2013 Concur Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/
$print-color: #999;
$print-color-light: #ccc;
$print-font-size: 12px;
body {
@extend %default-font;
}
.tocify, .toc-footer, .lang-selector, .search, #nav-button {
display: none;
}
.tocify-wrapper>img {
margin: 0 auto;
display: block;
}
.content {
font-size: 12px;
pre, code {
@extend %code-font;
@extend %break-words;
border: 1px solid $print-color;
border-radius: 5px;
font-size: 0.8em;
}
pre {
code {
border: 0;
}
}
pre {
padding: 1.3em;
}
code {
padding: 0.2em;
}
table {
border: 1px solid $print-color;
tr {
border-bottom: 1px solid $print-color;
}
td,th {
padding: 0.7em;
}
}
p {
line-height: 1.5;
}
a {
text-decoration: none;
color: #000;
}
h1 {
@extend %header-font;
font-size: 2.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-top: 1em;
margin-bottom: $h1-margin-bottom;
border: 2px solid $print-color-light;
border-width: 2px 0;
text-align: center;
}
h2 {
@extend %header-font;
font-size: 1.8em;
margin-top: 2em;
border-top: 2px solid $print-color-light;
padding-top: 0.8em;
}
h1+h2, h1+div+h2 {
border-top: none;
padding-top: 0;
margin-top: 0;
}
h3, h4 {
@extend %header-font;
font-size: 0.8em;
margin-top: 1.5em;
margin-bottom: 0.8em;
text-transform: uppercase;
}
h5, h6 {
text-transform: uppercase;
}
aside {
padding: 1em;
border: 1px solid $print-color-light;
border-radius: 5px;
margin-top: 1.5em;
margin-bottom: 1.5em;
line-height: 1.6;
}
aside:before {
vertical-align: middle;
padding-right: 0.5em;
font-size: 14px;
}
aside.notice:before {
@extend %icon-info-sign;
}
aside.warning:before {
@extend %icon-exclamation-sign;
}
aside.success:before {
@extend %icon-ok-sign;
}
}
.copy-clipboard {
@media print {
display: none
}
}

View File

@ -0,0 +1,633 @@
@charset "utf-8";
@import 'normalize';
@import 'variables';
@import 'icon-font';
// @import 'rtl'; // uncomment to switch to RTL format
/*
Copyright 2008-2013 Concur Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/
////////////////////////////////////////////////////////////////////////////////
// GENERAL STUFF
////////////////////////////////////////////////////////////////////////////////
html, body {
color: $main-text;
padding: 0;
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@extend %default-font;
background-color: $main-bg;
height: 100%;
-webkit-text-size-adjust: none; /* Never autoresize text */
}
////////////////////////////////////////////////////////////////////////////////
// TABLE OF CONTENTS
////////////////////////////////////////////////////////////////////////////////
#toc > ul > li > a > span {
float: right;
background-color: #2484FF;
border-radius: 40px;
width: 20px;
}
.toc-wrapper {
transition: left 0.3s ease-in-out;
overflow-y: auto;
overflow-x: hidden;
position: fixed;
z-index: 30;
top: 0;
left: 0;
bottom: 0;
width: $nav-width;
background-color: $nav-bg;
font-size: 13px;
font-weight: bold;
// language selector for mobile devices
.lang-selector {
display: none;
a {
padding-top: 0.5em;
padding-bottom: 0.5em;
}
}
// This is the logo at the top of the ToC
.logo {
display: block;
max-width: 100%;
margin-bottom: $logo-margin;
}
&>.search {
position: relative;
input {
background: $nav-bg;
border-width: 0 0 1px 0;
border-color: $search-box-border-color;
padding: 6px 0 6px 20px;
box-sizing: border-box;
margin: $nav-v-padding $nav-padding;
width: $nav-width - ($nav-padding*2);
outline: none;
color: $nav-text;
border-radius: 0; /* ios has a default border radius */
}
&:before {
position: absolute;
top: 17px;
left: $nav-padding;
color: $nav-text;
@extend %icon-search;
}
}
.search-results {
margin-top: 0;
box-sizing: border-box;
height: 0;
overflow-y: auto;
overflow-x: hidden;
transition-property: height, margin;
transition-duration: 180ms;
transition-timing-function: ease-in-out;
background: $nav-subitem-bg;
&.visible {
height: 30%;
margin-bottom: 1em;
}
li {
margin: 1em $nav-padding;
line-height: 1;
}
a {
color: $nav-text;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
// The Table of Contents is composed of multiple nested
// unordered lists. These styles remove the default
// styling of an unordered list because it is ugly.
ul, li {
list-style: none;
margin: 0;
padding: 0;
line-height: 28px;
}
li {
color: $nav-text;
transition-property: background;
transition-timing-function: linear;
transition-duration: 200ms;
}
// This is the currently selected ToC entry
.toc-link.active {
background-color: $nav-active-bg;
color: $nav-active-text;
}
// this is parent links of the currently selected ToC entry
.toc-link.active-parent {
background-color: $nav-active-parent-bg;
color: $nav-active-parent-text;
}
.toc-list-h2 {
display: none;
background-color: $nav-subitem-bg;
font-weight: 500;
}
.toc-h2 {
padding-left: $nav-padding + $nav-indent;
font-size: 12px;
}
.toc-footer {
padding: 1em 0;
margin-top: 1em;
border-top: 1px dashed $nav-footer-border-color;
li,a {
color: $nav-text;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
li {
font-size: 0.8em;
line-height: 1.7;
text-decoration: none;
}
}
}
.toc-link, .toc-footer li {
padding: 0 $nav-padding 0 $nav-padding;
display: block;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-decoration: none;
color: $nav-text;
transition-property: background;
transition-timing-function: linear;
transition-duration: 130ms;
}
// button to show navigation on mobile devices
#nav-button {
span {
display: block;
$side-pad: $main-padding / 2 - 8px;
padding: $side-pad $side-pad $side-pad;
background-color: rgba($main-bg, 0.7);
transform-origin: 0 0;
transform: rotate(-90deg) translate(-100%, 0);
border-radius: 0 0 0 5px;
}
padding: 0 1.5em 5em 0; // increase touch size area
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 100;
color: #000;
text-decoration: none;
font-weight: bold;
opacity: 0.7;
line-height: 16px;
img {
height: 16px;
vertical-align: bottom;
}
transition: left 0.3s ease-in-out;
&:hover { opacity: 1; }
&.open {left: $nav-width}
}
////////////////////////////////////////////////////////////////////////////////
// PAGE LAYOUT AND CODE SAMPLE BACKGROUND
////////////////////////////////////////////////////////////////////////////////
.page-wrapper {
margin-left: $nav-width;
position: relative;
z-index: 10;
background-color: $main-bg;
min-height: 100%;
padding-bottom: 1px; // prevent margin overflow
// The dark box is what gives the code samples their dark background.
// It sits essentially under the actual content block, which has a
// transparent background.
// I know, it's hackish, but it's the simplist way to make the left
// half of the content always this background color.
.dark-box {
width: $examples-width;
background-color: $examples-bg;
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
.lang-selector {
position: fixed;
z-index: 50;
border-bottom: 5px solid $lang-select-active-bg;
}
}
.lang-selector {
display: flex;
background-color: $lang-select-bg;
width: 100%;
font-weight: bold;
overflow-x: auto;
a {
display: inline;
color: $lang-select-text;
text-decoration: none;
padding: 0 10px;
line-height: 30px;
outline: 0;
&:active, &:focus {
background-color: $lang-select-pressed-bg;
color: $lang-select-pressed-text;
}
&.active {
background-color: $lang-select-active-bg;
color: $lang-select-active-text;
}
}
&:after {
content: '';
clear: both;
display: block;
}
}
////////////////////////////////////////////////////////////////////////////////
// CONTENT STYLES
////////////////////////////////////////////////////////////////////////////////
// This is all the stuff with the light background in the left half of the page
.content {
// fixes webkit rendering bug for some: see #538
-webkit-transform: translateZ(0);
// to place content above the dark box
position: relative;
z-index: 30;
&:after {
content: '';
display: block;
clear: both;
}
&>h1, &>h2, &>h3, &>h4, &>h5, &>h6, &>p, &>table, &>ul, &>ol, &>aside, &>dl {
margin-right: $examples-width;
padding: 0 $main-padding;
box-sizing: border-box;
display: block;
@extend %left-col;
}
&>ul, &>ol {
padding-left: $main-padding + 15px;
}
// the div is the tocify hidden div for placeholding stuff
&>h1, &>h2, &>div {
clear:both;
}
h1 {
@extend %header-font;
font-size: 25px;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-bottom: $h1-margin-bottom;
margin-top: 2em;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
background-color: #fdfdfd;
}
h1:first-child, div:first-child + h1 {
border-top-width: 0;
margin-top: 0;
}
h2 {
@extend %header-font;
font-size: 19px;
margin-top: 4em;
margin-bottom: 0;
border-top: 1px solid #ccc;
padding-top: 1.2em;
padding-bottom: 1.2em;
background-image: linear-gradient(to bottom, rgba(#fff, 0.2), rgba(#fff, 0));
}
// h2s right after h1s should bump right up
// against the h1s.
h1 + h2, h1 + div + h2 {
margin-top: $h1-margin-bottom * -1;
border-top: none;
}
h3, h4, h5, h6 {
@extend %header-font;
font-size: 15px;
margin-top: 2.5em;
margin-bottom: 0.8em;
}
h4, h5, h6 {
font-size: 10px;
}
hr {
margin: 2em 0;
border-top: 2px solid $examples-bg;
border-bottom: 2px solid $main-bg;
}
table {
margin-bottom: 1em;
overflow: auto;
th,td {
text-align: left;
vertical-align: top;
line-height: 1.6;
code {
white-space: nowrap;
}
}
th {
padding: 5px 10px;
border-bottom: 1px solid #ccc;
vertical-align: bottom;
}
td {
padding: 10px;
}
tr:last-child {
border-bottom: 1px solid #ccc;
}
tr:nth-child(odd)>td {
background-color: lighten($main-bg,4.2%);
}
tr:nth-child(even)>td {
background-color: lighten($main-bg,2.4%);
}
}
dt {
font-weight: bold;
}
dd {
margin-left: 15px;
}
p, li, dt, dd {
line-height: 1.6;
margin-top: 0;
}
img {
max-width: 100%;
}
code {
background-color: rgba(0,0,0,0.05);
padding: 3px;
border-radius: 3px;
@extend %break-words;
@extend %code-font;
}
pre>code {
background-color: transparent;
padding: 0;
}
aside {
padding-top: 1em;
padding-bottom: 1em;
margin-top: 1.5em;
margin-bottom: 1.5em;
background: $aside-notice-bg;
line-height: 1.6;
&.warning {
background-color: $aside-warning-bg;
}
&.success {
background-color: $aside-success-bg;
}
}
aside:before {
vertical-align: middle;
padding-right: 0.5em;
font-size: 14px;
}
aside.notice:before {
@extend %icon-info-sign;
}
aside.warning:before {
@extend %icon-exclamation-sign;
}
aside.success:before {
@extend %icon-ok-sign;
}
.search-highlight {
padding: 2px;
margin: -3px;
border-radius: 4px;
border: 1px solid #F7E633;
background: linear-gradient(to top left, #F7E633 0%, #F1D32F 100%);
}
}
////////////////////////////////////////////////////////////////////////////////
// CODE SAMPLE STYLES
////////////////////////////////////////////////////////////////////////////////
// This is all the stuff that appears in the right half of the page
.content {
&>div.highlight {
clear:none;
}
pre, blockquote {
background-color: $code-bg;
color: #fff;
margin: 0;
width: $examples-width;
float:right;
clear:right;
box-sizing: border-box;
@extend %right-col;
&>p { margin: 0; }
a {
color: #fff;
text-decoration: none;
border-bottom: dashed 1px #ccc;
}
}
pre {
@extend %code-font;
padding-top: 2em;
padding-bottom: 2em;
padding: 2em $main-padding;
}
blockquote {
&>p {
background-color: $code-annotation-bg;
padding: $code-annotation-padding 2em;
color: #eee;
}
}
}
////////////////////////////////////////////////////////////////////////////////
// RESPONSIVE DESIGN
////////////////////////////////////////////////////////////////////////////////
// These are the styles for phones and tablets
// There are also a couple styles disperesed
@media (max-width: $tablet-width) {
.toc-wrapper {
left: -$nav-width;
&.open {
left: 0;
}
}
.page-wrapper {
margin-left: 0;
}
#nav-button {
display: block;
}
.toc-link {
padding-top: 0.3em;
padding-bottom: 0.3em;
}
}
@media (max-width: $phone-width) {
.dark-box {
display: none;
}
%left-col {
margin-right: 0;
}
.toc-wrapper .lang-selector {
display: block;
}
.page-wrapper .lang-selector {
display: none;
}
%right-col {
width: auto;
float: none;
}
%right-col + %left-col {
margin-top: $main-padding;
}
}
.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs {
color: #909090;
}
.highlight, .highlight .w {
background-color: $code-bg;
}
.copy-clipboard {
float: right;
fill: #9DAAB6;
cursor: pointer;
opacity: 0.4;
height: 18px;
width: 18px;
}
.copy-clipboard:hover {
opacity: 0.8;
}

View File

@ -1,11 +1,12 @@
# hugo server --minify --themesDir ... --baseURL=http://0.0.0.0:1313/theme/hugo-book/
baseURL = 'https://rustybever.be/docs/vieter/'
title = 'The Rusty Bever - Docs'
title = 'Vieter - Docs'
theme = 'hugo-book'
# Book configuration
disablePathToLower = true
# Doesn't work with docs as subdir
enableGitInfo = true
# Needed for mermaid/katex shortcodes
@ -27,16 +28,22 @@ enableGitInfo = true
weight = 1
[menu]
# [[menu.before]]
[[menu.after]]
name = "Source"
url = "https://git.rustybever.be/Chewing_Bever/docs"
name = "HTTP API Docs"
url = "https://rustybever.be/docs/vieter/api/"
weight = 10
[[menu.after]]
name = "Man Pages"
url = "https://rustybever.be/man/vieter/vieter.1.html"
weight = 20
[[menu.after]]
name = "Vieter"
url = "https://git.rustybever.be/vieter/vieter"
weight = 30
[[menu.after]]
name = "Hugo Theme"
url = "https://github.com/alex-shpak/hugo-book"
weight = 20
weight = 40
[params]
# (Optional, default light) Sets color theme: light, dark or auto.
@ -63,14 +70,14 @@ enableGitInfo = true
# Set source repository location.
# Used for 'Last Modified' and 'Edit this page' links.
BookRepo = 'https://git.rustybever.be/Chewing_Bever/docs'
BookRepo = 'https://git.rustybever.be/vieter/vieter'
# (Optional, default 'commit') Specifies commit portion of the link to the page's last modified
# commit hash for 'doc' page type.
# Requires 'BookRepo' param.
# Value used to construct a URL consisting of BookRepo/BookCommitPath/<commit-hash>
# Github uses 'commit', Bitbucket uses 'commits'
# BookCommitPath = 'commit'
BookCommitPath = 'src/commit'
# Enable "Edit this page" links for 'doc' page type.
# Disabled by default. Uncomment to enable. Requires 'BookRepo' param.

View File

@ -1,27 +0,0 @@
# Vieter CLI
I provide a simple CLI tool that currently only allows changing the Git
repository API. Its usage is quite simple.
First, you need to create a file in your home directory called `.vieterrc` with
the following content:
```toml
address = "https://example.com"
api_key = "your-api-key"
```
You can also use a different file or use environment variables, as described in
[Configuration](/configuration).
Now you're ready to use the CLI tool.
## Usage
* `vieter repos list` returns all repositories currently stored in the API.
* `vieter repos add url branch repo arch...` adds the repository with the given
URL, branch, repo & arch to the API.
* `vieter repos remove id` removes the repository with the given ID prefix.
You can always check `vieter -help` or `vieter repos -help` for more
information about the commands.

View File

@ -9,12 +9,9 @@ documentation might not be relevant anymore for the latest release.
## Overview
Vieter has a few main features:
* It's a simple & lightweight implementation of an Arch repository server
* It allows for uploading of built package archives
* It supports a basic build system to periodically re-build packages & upload
them to the server
Vieter consists of two main parts, namely an implementation of an Arch
repository server & a scheduling system to periodically build Pacman packages &
publish them to a repository.
{{< hint info >}}
**Note**
@ -26,12 +23,12 @@ well.
### Why?
Vieter is my personal solution for a problem I've been facing for months:
Vieter is my personal solution to a problem I've been facing for months:
extremely long AUR package build times. I run EndeavourOS on both my laptops,
one of which being a rather old MacBook Air. I really like being a beta-tester
for projects & run development builds for multiple packages (nheko,
newsflash...). The issue with this is that I have to regularly re-build these
packages in order to stay up to date with development & these builds can take a
newsflash...). Because of this, I have to regularly re-build these packages in
order to stay up to date with development. However, these builds can take a
really long time on the old MacBook. This project is a solution to that
problem: instead of building the packages locally, I can build them
automatically in the cloud & just download them whenever I update my system!

View File

@ -1,89 +0,0 @@
# API Reference
All routes that return JSON use the following shape:
```json
{
"message": "some message",
"data": {}
}
```
Here, data can be any JSON object, so it's not guaranteed to be a struct.
### `GET /<repo>/<arch>/<filename>`
This route serves the contents of a specific architecture' repo.
If `<filename>` is one of `<repo>.db`, `<repo>.files`, `<repo>.db.tar.gz` or
`<repo>.files.tar.gz`, it will serve the respective archive file from the
repository.
If `<filename>` contains `.pkg`, it assumes the request to be for a package
archive & will serve that file from the specific arch-repo's package directory.
Finally, if none of the above are true, Vieter assumes it to be request for a
package version's desc file & tries to serve this instead. This functionality
is very useful for the build system for checking whether a package needs to be
rebuilt or not.
### `HEAD /<repo>/<arch>/<filename>`
Behaves the same as the above route, but instead of returning actual data, it
returns either 200 or 404, depending on whether the file exists. This route is
used by the build system to determine whether a package needs to be rebuilt.
### `POST /<repo>/publish`
This route is used to upload packages to a repository. It requires the API
key to be provided using the `X-Api-Key` HTTP header. Vieter will parse the
package's contents & update the repository files accordingely. I find the
easiest way to use this route is using cURL:
```sh
curl -XPOST -T "path-to-package.pkg.tar.zst" -H "X-API-KEY: your-api-key" https://example.com/somerepo/publish
```
Packages are automatically added to the correct arch-repo. If a package type is
`any`, the package is added to the configured `default_arch`, as well as all
already present arch-repos. To prevent unnecessary duplication of package
files, these packages are shared between arch-repos' package directories using
hard links.
{{< hint info >}}
**Note**
Vieter only supports uploading archives compressed using either gzip, zstd or
xz at the moment.
{{< /hint >}}
### `GET /health`
This endpoint's only use is to be used with healthchecks. It returns a JSON
response with the message "Healthy.".
## API
All API routes require the API key to provided using the `X-Api-Key` header.
Otherwise, they'll return a status code 401.
### `GET /api/repos`
Returns the current list of Git repositories.
### `GET /api/repos/<id>`
Get the information for the Git repo with the given ID.
### `POST /api/repos?<url>&<branch>&<arch>&<repo>`
Adds a new Git repository with the provided URL, Git branch & comma-separated
list of architectures.
### `DELETE /api/repos/<id>`
Deletes the Git repository with the provided ID.
### `PATCH /api/repos/<id>?<url>&<branch>&<arch>&<repo>`
Updates the provided parameters for the repo with the given ID. All arguments
are optional.

View File

@ -1,56 +0,0 @@
# Builder
Vieter supports a basic build system that allows you to build the packages
defined using the Git repositories API by running `vieter build`. For
configuration, see [here](/configuration#builder).
## How it works
The build system works in two stages. First it pulls down the
`archlinux:latest` image from Docker Hub, runs `pacman -Syu` & configures a
non-root build user. It then creates a new Docker image from this container.
This is to prevent each build having to fully update the container's
repositories. After the image has been created, each repository returned by
`/api/repos` is built sequentially by starting up a new container with the
previously created image as a base. Each container goes through the following steps:
1. The repository is cloned
2. `makepkg --nobuild --syncdeps --needed --noconfirm` is ran to update the `pkgver` variable inside
the `PKGBUILD` file
3. A HEAD request is sent to the Vieter server to check whether the specific
version of the package is already present. If it is, the container exits.
4. `makepkg` is ran with `MAKEFLAGS="-j\$(nproc)`
5. Each produced package archive is uploaded to the Vieter instance's
repository, as defined in the API for that specific Git repo.
## Cron image
The Vieter Docker image contains crond & a cron config that runs `vieter build`
every night at 3AM. This value is currently hardcoded, but I wish to change
that down the line (work is in progress). There's also some other caveats you
should be aware of, namely that the image should be run as root & that the
healthcheck will always fail, so you might have to disable it. This boils down
to the following docker-compose file:
```yaml
version: '3'
services:
cron:
image: 'chewingbever/vieter:dev'
command: crond -f
user: root
healthcheck:
disable: true
environment:
- 'VIETER_API_KEY=some-key'
- 'VIETER_ADDRESS=https://example.com'
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
```
Important to note is that the container also requires the host's Docker socket
to be mounted as this is how it spawns the necessary containers, as well as a
change to the container's command.

View File

@ -3,7 +3,7 @@ weight: 20
---
# Configuration
All vieter operations by default try to read in the TOML file `~/.vieterrc` for
By default, all vieter commands try to read in the TOML file `~/.vieterrc` for
configuration. The location of this file can be changed by using the `-f` flag.
If the above file doesn't exist or you wish to override some of its settings,
@ -19,53 +19,80 @@ the value in the environment variable is used.
{{< hint info >}}
**Note**
All environment variables can also be provided from a file by appending them
with `_FILE`. This for example allows you to provide the API key from a docker
with `_FILE`. This for example allows you to provide the API key from a Docker
secrets file.
{{< /hint >}}
## Modes
## Commands
The vieter binary can run in several "modes", indicated by the first argument
passed to them. Each mode requires a different configuration.
The first argument passed to Vieter determines which command you wish to use.
Each of these can contain subcommands (e.g. `vieter repos list`), but all
subcommands will use the same configuration. Below you can find the
configuration variable required for each command.
### Server
### `vieter server`
* `log_level`: defines how much logs to show. Valid values are one of `FATAL`,
`ERROR`, `WARN`, `INFO` or `DEBUG`. Defaults to `WARN`
* `log_file`: log file to write logs to. Defaults to `vieter.log` in the
current directory.
* `log_level`: log verbosity level. Value should be one of `FATAL`, `ERROR`,
`WARN`, `INFO` or `DEBUG`.
* Default: `WARN`
* `log_file`: log file to write logs to.
* Default: `vieter.log` (in the current directory)
* `pkg_dir`: where Vieter should store the actual package archives.
* `data_dir`: where Vieter stores the repositories, log file & database.
* `api_key`: the API key to use when authenticating requests.
* `default_arch`: architecture to always add packages of arch `any` to.
* `default_arch`: this setting serves two main purposes:
* Packages with architecture `any` are always added to this architecture.
This prevents the server from being confused when an `any` package is
published as the very first package for a repository.
* Git repositories added without an `arch` value use this value instead.
### Builder
* `api_key`: the API key to use when authenticating requests.
* `address`: Base your URL of your Vieter instance, e.g. https://example.com
* `base_image`: image to use when building a package. It should be an Archlinux
image. The default if not configured is `archlinux:base-devel`, but this
image only supports arm64. If you require aarch64 support as well, consider
using
### `vieter cron`
* `log_level`: log verbosity level. Value should be one of `FATAL`, `ERROR`,
`WARN`, `INFO` or `DEBUG`.
* Default: `WARN`
* `log_file`: log file to write logs to.
* Default: `vieter.log` (in `data_dir`)
* `address`: *public* URL of the Vieter repository server to build for. From
this server the list of Git repositories is retrieved. All built packages are
published to this server.
* `api_key`: API key of the above server.
* `data_dir`: directory to store log file in.
* `base_image`: Docker image to use when building a package. Any Pacman-based
distro image should work, as long as `/etc/pacman.conf` is used &
`base-devel` exists in the repositories. Make sure that the image supports
the architecture of your cron daemon.
* Default: `archlinux:base-devel` (only works on `x86_64`). If you require
`aarch64` support, consider using
[`menci/archlinuxarm:base-devel`](https://hub.docker.com/r/menci/archlinuxarm)
([GH](https://github.com/Menci/docker-archlinuxarm))
([GitHub](https://github.com/Menci/docker-archlinuxarm)). This is the image
used for the Vieter CI builds.
* `max_concurrent_builds`: how many builds to run at the same time.
* Default: `1`
* `api_update_frequency`: how frequently (in minutes) to poll the Vieter
repository server for a new list of Git repositories to build.
* Default: `15`
* `image_rebuild_frequency`: Vieter periodically builds a builder image using
the configured base image. This makes sure build containers do not have to
download a lot of packages when updating their system. This setting defines
how frequently (in minutes) to rebuild this builder image.
* Default: `1440` (every 24 hours)
* `global_schedule`: build schedule for any Git repository that does not have a
schedule defined. For information about this syntax, see
[here](/usage/builds/schedule).
* Default: `0 3` (3AM every night)
### Repos
### `vieter logs`
* `api_key`: the API key to use when authenticating requests.
* `address`: Base your URL of your Vieter instance, e.g. https://example.com
* `address`: Base URL of your Vieter instance, e.g. https://example.com
### Cron
### `vieter repos`
* `log_level`: defines how much logs to show. Valid values are one of `FATAL`,
`ERROR`, `WARN`, `INFO` or `DEBUG`. Defaults to `WARN`
* `api_key`: the API key to use when authenticating requests.
* `address`: Base your URL of your Vieter instance, e.g. https://example.com.
This *must* be the publicly facing URL of your Vieter instance.
* `data_dir`: where Vieter stores the log file.
* `base_image`: Docker image from which to create the builder images.
* `max_concurrent_builds`: amount of builds to run at once.
* `api_update_frequency`: how frequenty to check for changes in the repo list.
* `image_rebuild+frequency`: how frequently to rebuild the builder image
* `global_schedule`: cron schedule to use for any repo without an individual
schedule
* `address`: Base URL of your Vieter instance, e.g. https://example.com
* `base_image`: image to use when building a package using `vieter repos
build`.
* Default: `archlinux:base-devel`

View File

@ -3,76 +3,100 @@ weight: 10
---
# Installation
Vieter consists of a single binary, akin to busybox. The binary's behavior is
determined by its CLI arguments, e.g. `vieter server` starts the repository
server.
All installation solutions can be configured the same way,
as described [here](/configuration).
## Docker
Docker is the recommended way to install vieter. The images can be pulled from
[`chewingbever/vieter`](https://hub.docker.com/r/chewingbever/vieter). You can
either pull a release tag (e.g. `chewingbever/vieter:0.1.0-rc1`), or pull the
`chewingbever/vieter:dev` tag. The latter is updated every time a new commit is
pushed to the development branch. This branch will be the most up to date, but
does not give any guarantees about stability, so beware!
Docker images are published to the
[`chewingbever/vieter`](https://hub.docker.com/r/chewingbever/vieter) Docker
Hub repository. You can either pull a release tag (e.g.
`chewingbever/vieter:0.1.0-rc1`), or pull the `chewingbever/vieter:dev` tag.
The latter is updated every time a new commit is pushed to the development
branch. This branch will be the most up to date, but does not give any
guarantees about stability, so beware!
The simplest way to run the Docker image is using a plain Docker command:
Thanks to the single-binary design of Vieter, this image can be used both for
the repository server & the cron daemon.
```sh
docker run \
--rm \
-d \
-v /path/to/data:/data \
-e VIETER_API_KEY=changeme \
-e VIETER_DEFAULT_ARCH=x86_64 \
-p 8000:8000 \
chewingbever/vieter:dev
Below is an example compose file to set up both the repository server & the
cron daemon:
```yaml
version: '3'
services:
server:
image: 'chewingbever/vieter:dev'
restart: 'always'
environment:
- 'VIETER_API_KEY=secret'
- 'VIETER_DEFAULT_ARCH=x86_64'
volumes:
- 'data:/data'
cron:
image: 'chewingbever/vieter:dev'
restart: 'always'
user: root
command: 'vieter cron'
environment:
- 'VIETER_API_KEY=secret'
# MUST be public URL of Vieter repository
- 'VIETER_ADDRESS=https://example.com'
- 'VIETER_DEFAULT_ARCH=x86_64'
- 'VIETER_MAX_CONCURRENT_BUILDS=2'
- 'VIETER_GLOBAL_SCHEDULE=0 3'
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
volumes:
data:
```
Here, you should change `/path/to/data` to the path on your host where you want
vieter to store its files.
If you do not require the build system, the repository server can be used
independently as well.
The default configuration will store everything inside the `/data` directory.
Inside the container, the Vieter server runs on port 8000. This port should be
exposed to the public accordingely.
For an overview of how to configure vieter & which environment variables can be
used, see the [Configuration](/configuration) page.
{{< hint info >}}
**Note**
Builds are executed on the cron daemon's system using the host's Docker daemon.
A cron daemon on a specific architecture will only build packages for that
specific architecture. Therefore, if you wish to build packages for both
`x86_64` & `aarch64`, you'll have to deploy two cron daemons, one on each
architecture. Afterwards, any Git repositories enabled for those two
architectures will build on both.
{{< /hint >}}
## Binary
On the [releases](https://git.rustybever.be/Chewing_Bever/vieter/releases)
page, you can find statically compiled binaries for all released versions. You
can download the binary for your host's architecture & run it that way.
On the
[releases](https://git.rustybever.be/vieter/vieter/releases)
page, you can find statically compiled binaries for all
released versions. This is the same binary as used inside
the Docker images.
For more information about configuring the binary, check out the
[Configuration](/configuration) page.
## Arch
I publish both development & release versions of Vieter to my personal
repository, https://arch.r8r.be. Packages are available for `x86_64` &
`aarch64`. To use the repository, add the following to your `pacman.conf`:
```
[vieter]
Server = https://arch.r8r.be/$repo/$arch
SigLevel = Optional
```
Afterwards, you can update your system & install the `vieter` package for the
latest official release or `vieter-git` for the latest development release.
## Building from source
Because the project is still in heavy development, it might be useful to build
from source instead. Luckily, this process is very easy. You'll need make,
libarchive & openssl; all of which should be present on an every-day Arch
install. Then, after cloning the repository, you can use the following commands:
```sh
# Builds the compiler; should usually only be ran once. Vieter compiles using
# the default compiler, but I maintain my own mirror to ensure nothing breaks
# without me knowing.
make v
# Build vieter
# Alternatively, use `make prod` to build the production build.
make
```
{{< hint info >}}
**Note**
My version of the V compiler is also available on my Vieter instance,
https://arch.r8r.be. It's in the `vieter` repository, with the package being
named `vieter-v`. The compiler is available for both x86_64 & aarch64.
{{< /hint >}}
## My Vieter instance
Besides uploading development Docker images, my CI also publishes x86_64 &
aarch64 packages to my personal Vieter instance, https://arch.r8r.be. If you'd
like, you can use this repository as well by adding it to your Pacman
configuration as described [here](/usage#configuring-pacman). Both the
repository & the package are called `vieter`.
The project [README](https://git.rustybever.be/vieter/vieter#building) contains
instructions for building Vieter from source.

View File

@ -0,0 +1,3 @@
---
weight: 100
---

View File

@ -0,0 +1,81 @@
# Builds In-depth
For those interested, this page describes how the build system works
internally.
## Builder image
Every cron daemon perodically creates a builder image that is then used as a
base for all builds. This is done to prevent build containers having to pull
down a bunch of updates when they update their system.
The build container is created by running the following commands inside a
container started from the image defined in `base_image`:
```sh
# Update repos & install required packages
pacman -Syu --needed --noconfirm base-devel git
# Add a non-root user to run makepkg
groupadd -g 1000 builder
useradd -mg builder builder
# Make sure they can use sudo without a password
echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
# Create the directory for the builds & make it writeable for the
# build user
mkdir /build
chown -R builder:builder /build
```
This script updates the packages to their latest versions & creates a non-root
user to use when running `makepkg`.
This script is base64-encoded & passed to the container as an environment
variable. The container's entrypoint is set to `/bin/sh -c` & its command
argument to `echo $BUILD_SCRIPT | base64 -d | /bin/sh -e`, with the
`BUILD_SCRIPT` environment variable containing the base64-encoded script.
Once the container exits, a new Docker image is created from it. This image is
then used as the base for any builds.
## Running builds
Each build has its own Docker container, using the builder image as its base.
The same base64-based technique as above is used, just with a different script.
To make the build logs more clear, each command is appended by an echo command
printing the next command to stdout.
Given the Git repository URL is `https://examplerepo.com` with branch `main`,
the URL of the Vieter server is `https://example.com` and `vieter` is the
repository we wish to publish to, we get the following script:
```sh
echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf'
echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf
echo -e '+ pacman -Syu --needed --noconfirm'
pacman -Syu --needed --noconfirm
echo -e '+ su builder'
su builder
echo -e '+ git clone --single-branch --depth 1 --branch main https://examplerepo.com repo'
git clone --single-branch --depth 1 --branch main https://examplerepo.com repo
echo -e '+ cd repo'
cd repo
echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm'
makepkg --nobuild --syncdeps --needed --noconfirm
echo -e '+ source PKGBUILD'
source PKGBUILD
echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0'
curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0
echo -e '+ [ "$(id -u)" == 0 ] && exit 0'
[ "$(id -u)" == 0 ] && exit 0
echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done'
MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done
```
This script:
1. Adds the target repository as a repository in the build container
2. Updates mirrors & packages
3. Clones the Git repository
4. Runs `makepkg` without building to calculate `pkgver`
5. Checks whether the package version is already present on the server
6. If not, run `makepkg` & publish any generated package archives to the server

View File

@ -1,54 +0,0 @@
---
weight: 30
---
# Usage
## Starting the server
To start a server, either install it using Docker (see
[Installation](/installation)) or run it locally by executing `vieter
server`. See [Configuration](/configuration) for more information about
configuring the binary.
## Multiple repositories
Vieter works with multiple repositories. This means that a single Vieter server
can serve multiple repositories in Pacman. It also automatically divides files
with specific architectures among arch-repos. Arch-repos are the actual
repositories you add to your `/etc/pacman.conf` file. See [Configuring
Pacman](/usage#configuring-pacman) below for more info.
## Adding packages
Using Vieter is currently very simple. If you wish to add a package to Vieter,
build it using makepkg & POST that file to the `/<repo>/publish` endpoint of
your server. This will add the package to the repository. Authentification
requires you to add the API key as the `X-Api-Key` header.
All of this can be combined into a simple cURL call:
```
curl -XPOST -H "X-API-KEY: your-key" -T some-package.pkg.tar.zst https://example.com/somerepo/publish
```
`somerepo` is automatically created if it doesn't exist yet.
## Configuring Pacman
Configuring Pacman to use a Vieter instance is very simple. In your
`/etc/pacman.conf` file, add the following lines:
```
[vieter]
Server = https://example.com/$repo/$arch
SigLevel = Optional
```
Here, you see two important placeholder variables. `$repo` is replaced by the
name within the square brackets, which in this case would be `vieter`. `$arch`
is replaced by the output of `uname -m`. Because Vieter supports multiple
repositories & architectures per repository, using this notation makes sure you
always use the correct endpoint for fetching files.
I recommend placing this below all other repository entries, as the order
decides which repository should be used if there's ever a naming conflict.

View File

@ -0,0 +1,3 @@
---
weight: 30
---

View File

@ -0,0 +1,51 @@
---
weight: 20
---
# Building packages
The automatic build system is what makes Vieter very useful as a replacement
for an AUR helper. It can perodically build packages & publish them to your
personal Vieter repository server, removing the need to build the packages
locally.
## Adding builds
Before the cron system can start building your package, you need to add its
info to the system. The Vieter repository server exposes an HTTP API for this
(see the [HTTP API Docs](https://rustybever.be/docs/vieter/api/) for more
info). For ease of use, the Vieter binary contains a CLI interface for
interacting with this API (see [Configuration](/configuration) for
configuration details). The [man
pages](https://rustybever.be/man/vieter/vieter-repos.1.html) describe this in
greater detail, but the basic usage is as follows:
```
vieter repos add some-url some-branch some-repository
```
Here, `some-url` is the URL of the Git repository containing the PKGBUILD. This
URL is passed to `git clone`, meaning the repository should be public. Vieter
expects the same format as an AUR Git repository, so you can directly use AUR
URLs here.
`some-branch` is the branch of the Git repository the build should check out.
If you're using an AUR package, this should be `master`.
Finally, `some-repo` is the repository to which the built package archives
should be published.
The above command intentionally leaves out a few parameters to make the CLI
more useable. For information on how to modify all parameters using the CLI,
see
[vieter-repos-edit(1)](https://rustybever.be/man/vieter/vieter-repos-edit.1.html).
## Reading logs
The logs of each build are uploaded to the Vieter repository server, along with
information about the exit code of the build container, when the build
started/ended etc. These logs can then be accessed using the [HTTP
API](https://rustybever.be/docs/vieter/api/).
For ease of use, the logs are also available using some CLI commands; see
[vieter-logs(1)](https://rustybever.be/man/vieter/vieter-logs.1.html) for more
information.

View File

@ -0,0 +1,42 @@
# Cron schedule syntax
The Vieter cron daemon uses a subset of the cron expression syntax to schedule
builds.
## Format
`a b c d`
* `a`: minutes
* `b`: hours
* `c`: days
* `d`: months
An expression consists of two to four sections. If less than four sections are
provided, the parser will append `*` until there are four sections. This means
that `0 3` is the same as `0 3 * *`.
Each section consists of one or more parts, separated by a comma. Each of these
parts, in turn, can be one of the following (any letters are integers):
* `*`: allow all possible values.
* `a`: only this value is allowed.
* `*/n`: allow every n-th value.
* `a/n`: allow every n-th value, starting at a in the list.
* `a-b`: allow every value between a and b, bounds included.
* `a-b/n`: allow every n-th value inside the list of values between a and b,
bounds included.
Each section can consist of as many of these parts as necessary.
## Examples
* `0 3`: every day at 03:00AM.
* `0 0 */7`: every 7th day of the month, at midnight.
## CLI tool
The Vieter binary contains a command that shows you the next matching times for
a given expression. This can be useful to understand the syntax. For more
information, see
[vieter-schedule(1)](https://rustybever.be/man/vieter/vieter-schedule.1.html).

View File

@ -0,0 +1,41 @@
---
weight: 10
---
# Pacman repository
The part of Vieter that users will interact with the most is the Pacman
repository aka `vieter server`.
## Design overview
A Vieter repository server has support for multiple repositories, with each
repository containing packages for multiple architectures.
If you wish to use these repositories on your system, add the following to
`/etc/pacman.conf` for each repository you wish to use:
```
[repo-name]
Server = https://example.com/$repo/$arch
SigLevel = Optional
```
Here, `$repo` and `$arch` are not variables you have to fill in yourself.
Rather, Pacman will substitute these when reading the config file. `$repo` is
replaced by the name between the square brackets (in this case `repo-name`),
and `$arch` is replaced by your system's architecture, e.g. `x86_64`. Of
course, you can also fill in these values manually yourself, e.g. if you wish
to use a different name inside the square brackets.
Important to note is that, when two repositories contain a package with the
same name, Pacman will choose the one from the repository that's highest up in
the `pacman.conf` file. Therefore, if you know your repository has packages
with the same name as ones from the official repositories, it might be better
to place the repository below the official repositories to avoid overwriting
official packages.
## Publishing packages
Packages can be easily published using a single HTTP POST request. Check out
the [HTTP API docs](https://rustybever.be/docs/vieter/api/) for more info on
these routes, including example cURL commands.

View File

@ -4,9 +4,9 @@ import docker
import encoding.base64
import time
import os
import db
import strings
import util
import models { GitRepo }
const (
container_build_dir = '/build'
@ -93,7 +93,7 @@ pub:
// build_repo builds, packages & publishes a given Arch package based on the
// provided GitRepo. The base image ID should be of an image previously created
// by create_build_image. It returns the logs of the container.
pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ?BuildResult {
pub fn build_repo(address string, api_key string, base_image_id string, repo &GitRepo) ?BuildResult {
mut dd := docker.new_conn()?
defer {
@ -101,30 +101,19 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db
}
build_arch := os.uname().machine
build_script := create_build_script(address, repo, build_arch)
// TODO what to do with PKGBUILDs that build multiple packages?
commands := [
'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo',
'cd repo',
'makepkg --nobuild --syncdeps --needed --noconfirm',
'source PKGBUILD',
// The build container checks whether the package is already
// present on the server
'curl -s --head --fail $address/$repo.repo/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0',
'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $address/$repo.repo/publish; done',
]
// We convert the list of commands into a base64 string, which then gets
// passed to the container as an env var
cmds_str := base64.encode_str(commands.join('\n'))
// We convert the build script into a base64 string, which then gets passed
// to the container as an env var
base64_script := base64.encode_str(build_script)
c := docker.NewContainer{
image: '$base_image_id'
env: ['BUILD_SCRIPT=$cmds_str', 'API_KEY=$api_key']
env: ['BUILD_SCRIPT=$base64_script', 'API_KEY=$api_key']
entrypoint: ['/bin/sh', '-c']
cmd: ['echo \$BUILD_SCRIPT | base64 -d | /bin/bash -e']
work_dir: '/build'
user: 'builder:builder'
user: '0:0'
}
id := dd.create_container(c)?.id

View File

@ -0,0 +1,20 @@
echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf'
echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf
echo -e '+ pacman -Syu --needed --noconfirm'
pacman -Syu --needed --noconfirm
echo -e '+ su builder'
su builder
echo -e '+ git clone --single-branch --depth 1 --branch main https://examplerepo.com repo'
git clone --single-branch --depth 1 --branch main https://examplerepo.com repo
echo -e '+ cd repo'
cd repo
echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm'
makepkg --nobuild --syncdeps --needed --noconfirm
echo -e '+ source PKGBUILD'
source PKGBUILD
echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0'
curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0
echo -e '+ [ "$(id -u)" == 0 ] && exit 0'
[ "$(id -u)" == 0 ] && exit 0
echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done'
MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done

55
src/build/shell.v 100644
View File

@ -0,0 +1,55 @@
module build
import models { GitRepo }
// escape_shell_string escapes any characters that could be interpreted
// incorrectly by a shell. The resulting value should be safe to use inside an
// echo statement.
fn escape_shell_string(s string) string {
return s.replace(r'\', r'\\').replace("'", r"'\''")
}
// echo_commands takes a list of shell commands & prepends each one with
// an echo call displaying said command.
pub fn echo_commands(cmds []string) []string {
mut out := []string{cap: 2 * cmds.len}
for cmd in cmds {
out << "echo -e '+ ${escape_shell_string(cmd)}'"
out << cmd
}
return out
}
// create_build_script generates a shell script that builds a given GitRepo.
fn create_build_script(address string, repo &GitRepo, build_arch string) string {
repo_url := '$address/$repo.repo'
commands := echo_commands([
// This will later be replaced by a proper setting for changing the
// mirrorlist
"echo -e '[$repo.repo]\\nServer = $address/\$repo/\$arch\\nSigLevel = Optional' >> /etc/pacman.conf"
// We need to update the package list of the repo we just added above.
// This should however not pull in a lot of packages as long as the
// builder image is rebuilt frequently.
'pacman -Syu --needed --noconfirm',
// makepkg can't run as root
'su builder',
'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo',
'cd repo',
'makepkg --nobuild --syncdeps --needed --noconfirm',
'source PKGBUILD',
// The build container checks whether the package is already present on
// the server.
'curl -s --head --fail $repo_url/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0',
// If the above curl command succeeds, we don't need to rebuild the
// package. However, because we're in a su shell, the exit command will
// drop us back into the root shell. Therefore, we must check whether
// we're in root so we don't proceed.
'[ "\$(id -u)" == 0 ] && exit 0',
'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $repo_url/publish; done',
])
return commands.join('\n')
}

View File

@ -0,0 +1,16 @@
module build
import models { GitRepo }
fn test_create_build_script() {
repo := GitRepo{
id: 1
url: 'https://examplerepo.com'
branch: 'main'
repo: 'vieter'
}
build_script := create_build_script('https://example.com', repo, 'x86_64')
expected := $embed_file('build_script.sh')
assert build_script == expected.to_string().trim_space()
}

View File

@ -29,8 +29,11 @@ fn (c &Client) send_request_raw(method Method, url string, params map[string]str
// Escape each query param
for k, v in params {
// An empty parameter should be the same as not providing it at all
if v != '' {
params_escaped[k] = urllib.query_escape(v)
}
}
params_str := params_escaped.keys().map('$it=${params[it]}').join('&')
@ -63,5 +66,5 @@ fn (c &Client) send_request_with_body<T>(method Method, url string, params map[s
fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string {
res := c.send_request_raw(method, url, params, body)?
return res.text
return res.body
}

View File

@ -1,16 +1,38 @@
module client
import db { GitRepo }
import models { GitRepo, GitRepoFilter }
import net.http { Method }
import response { Response }
// get_git_repos returns the current list of repos.
pub fn (c &Client) get_git_repos() ?[]GitRepo {
data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {})?
// get_git_repos returns a list of GitRepo's, given a filter object.
pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo {
params := models.params_from(filter)
data := c.send_request<[]GitRepo>(Method.get, '/api/repos', params)?
return data.data
}
// get_all_git_repos retrieves *all* GitRepo's from the API using the default
// limit.
pub fn (c &Client) get_all_git_repos() ?[]GitRepo {
mut repos := []GitRepo{}
mut offset := u64(0)
for {
sub_repos := c.get_git_repos(offset: offset)?
if sub_repos.len == 0 {
break
}
repos << sub_repos
offset += u64(sub_repos.len)
}
return repos
}
// get_git_repo returns the repo for a specific ID.
pub fn (c &Client) get_git_repo(id int) ?GitRepo {
data := c.send_request<GitRepo>(Method.get, '/api/repos/$id', {})?

View File

@ -1,13 +1,14 @@
module client
import db { BuildLog }
import models { BuildLog, BuildLogFilter }
import net.http { Method }
import response { Response }
import time
// get_build_logs returns all build logs.
pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> {
data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {})?
pub fn (c &Client) get_build_logs(filter BuildLogFilter) ?Response<[]BuildLog> {
params := models.params_from(filter)
data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)?
return data
}
@ -41,8 +42,8 @@ pub fn (c &Client) get_build_log_content(id int) ?string {
pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response<string> {
params := {
'repo': repo_id.str()
'startTime': start_time.str()
'endTime': end_time.str()
'startTime': start_time.unix_time().str()
'endTime': end_time.unix_time().str()
'arch': arch
'exitCode': exit_code.str()
}

View File

@ -2,6 +2,8 @@ module console
import arrays
import strings
import cli
import os
// pretty_table converts a list of string data into a pretty table. Many thanks
// to @hungrybluedev in the Vlang Discord for providing this code!
@ -54,3 +56,15 @@ pub fn pretty_table(header []string, data [][]string) ?string {
return buffer.str()
}
// export_man_pages recursively generates all man pages for the given
// cli.Command & writes them to the given directory.
pub fn export_man_pages(cmd cli.Command, path string) ? {
man := cmd.manpage()
os.write_file(os.join_path_single(path, cmd.full_name().replace(' ', '-') + '.1'),
man)?
for sub_cmd in cmd.commands {
export_man_pages(sub_cmd, path)?
}
}

View File

@ -5,6 +5,7 @@ import env
import cron.expression { parse_expression }
import client
import console
import models { GitRepoFilter }
struct Config {
address string [required]
@ -21,11 +22,45 @@ pub fn cmd() cli.Command {
cli.Command{
name: 'list'
description: 'List the current repos.'
flags: [
cli.Flag{
name: 'limit'
description: 'How many results to return.'
flag: cli.FlagType.int
},
cli.Flag{
name: 'offset'
description: 'Minimum index to return.'
flag: cli.FlagType.int
},
cli.Flag{
name: 'repo'
description: 'Only return Git repos that publish to this repo.'
flag: cli.FlagType.string
},
]
execute: fn (cmd cli.Command) ? {
config_file := cmd.flags.get_string('config-file')?
conf := env.load<Config>(config_file)?
list(conf)?
mut filter := GitRepoFilter{}
limit := cmd.flags.get_int('limit')?
if limit != 0 {
filter.limit = u64(limit)
}
offset := cmd.flags.get_int('offset')?
if offset != 0 {
filter.offset = u64(offset)
}
repo := cmd.flags.get_string('repo')?
if repo != '' {
filter.repo = repo
}
list(conf, filter)?
}
},
cli.Command{
@ -133,9 +168,9 @@ pub fn cmd() cli.Command {
// ID. If multiple or none are found, an error is raised.
// list prints out a list of all repositories.
fn list(conf Config) ? {
fn list(conf Config, filter GitRepoFilter) ? {
c := client.new(conf.address, conf.api_key)
repos := c.get_git_repos()?
repos := c.get_git_repos(filter)?
data := repos.map([it.id.str(), it.url, it.branch, it.repo])
println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?)

View File

@ -3,8 +3,9 @@ module logs
import cli
import env
import client
import db
import console
import time
import models { BuildLog, BuildLogFilter }
struct Config {
address string [required]
@ -19,21 +20,120 @@ pub fn cmd() cli.Command {
commands: [
cli.Command{
name: 'list'
description: 'List the build logs. If a repo ID is provided, only list the build logs for that repo.'
description: 'List build logs. All date strings in the output are converted to the local timezone. Any time strings provided as input should be in the local timezone as well.'
flags: [
cli.Flag{
name: 'repo'
description: 'ID of the Git repo to restrict list to.'
name: 'limit'
description: 'How many results to return.'
flag: cli.FlagType.int
},
cli.Flag{
name: 'offset'
description: 'Minimum index to return.'
flag: cli.FlagType.int
},
cli.Flag{
name: 'repo'
description: 'Only return logs for this repo id.'
flag: cli.FlagType.int
},
cli.Flag{
name: 'today'
description: 'Only list logs started today.'
flag: cli.FlagType.bool
},
cli.Flag{
name: 'failed'
description: 'Only list logs with non-zero exit codes.'
flag: cli.FlagType.bool
},
cli.Flag{
name: 'day'
description: 'Only list logs started on this day. (format: YYYY-MM-DD)'
flag: cli.FlagType.string
},
cli.Flag{
name: 'before'
description: 'Only list logs started before this timestamp. (format: YYYY-MM-DD HH:mm:ss)'
flag: cli.FlagType.string
},
cli.Flag{
name: 'after'
description: 'Only list logs started after this timestamp. (format: YYYY-MM-DD HH:mm:ss)'
flag: cli.FlagType.string
},
]
execute: fn (cmd cli.Command) ? {
config_file := cmd.flags.get_string('config-file')?
conf := env.load<Config>(config_file)?
repo_id := cmd.flags.get_int('repo')?
mut filter := BuildLogFilter{}
if repo_id == 0 { list(conf)? } else { list_for_repo(conf, repo_id)? }
limit := cmd.flags.get_int('limit')?
if limit != 0 {
filter.limit = u64(limit)
}
offset := cmd.flags.get_int('offset')?
if offset != 0 {
filter.offset = u64(offset)
}
repo_id := cmd.flags.get_int('repo')?
if repo_id != 0 {
filter.repo = repo_id
}
tz_offset := time.offset()
if cmd.flags.get_bool('today')? {
today := time.now()
filter.after = time.new_time(time.Time{
year: today.year
month: today.month
day: today.day
}).add_seconds(-tz_offset)
filter.before = filter.after.add_days(1)
}
// The -today flag overwrites any of the other date flags.
else {
day_str := cmd.flags.get_string('day')?
before_str := cmd.flags.get_string('before')?
after_str := cmd.flags.get_string('after')?
if day_str != '' {
day := time.parse_rfc3339(day_str)?
day_utc := time.new_time(time.Time{
year: day.year
month: day.month
day: day.day
}).add_seconds(-tz_offset)
// The extra -1 is so we also return logs that
// started at exactly midnight (filter bounds are
// exclusive). therefore, we have to request logs
// started after 23:59:59 the previous day.
filter.after = day_utc.add_seconds(-1)
filter.before = day_utc.add_days(1)
} else {
if before_str != '' {
filter.before = time.parse(before_str)?.add_seconds(-tz_offset)
}
if after_str != '' {
filter.after = time.parse(after_str)?.add_seconds(-tz_offset)
}
}
}
if cmd.flags.get_bool('failed')? {
filter.exit_codes = [
'!0',
]
}
list(conf, filter)?
}
},
cli.Command{
@ -67,17 +167,17 @@ pub fn cmd() cli.Command {
}
// print_log_list prints a list of logs.
fn print_log_list(logs []db.BuildLog) ? {
data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.str(),
fn print_log_list(logs []BuildLog) ? {
data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.local().str(),
it.exit_code.str()])
println(console.pretty_table(['id', 'repo', 'start time', 'exit code'], data)?)
}
// list prints a list of all build logs.
fn list(conf Config) ? {
fn list(conf Config, filter BuildLogFilter) ? {
c := client.new(conf.address, conf.api_key)
logs := c.get_build_logs()?.data
logs := c.get_build_logs(filter)?.data
print_log_list(logs)?
}

View File

@ -0,0 +1,21 @@
module man
import cli
import console
import os
// cmd returns the cli submodule that handles generating man pages.
pub fn cmd() cli.Command {
return cli.Command{
name: 'man'
description: 'Generate all man pages & save them in the given directory.'
usage: 'dir'
required_args: 1
execute: fn (cmd cli.Command) ? {
root := cmd.root()
os.mkdir_all(cmd.args[0])?
console.export_man_pages(root, cmd.args[0])?
}
}
}

View File

@ -0,0 +1,30 @@
module schedule
import cli
import cron.expression { parse_expression }
import time
// cmd returns the cli submodule for previewing a cron schedule.
pub fn cmd() cli.Command {
return cli.Command{
name: 'schedule'
usage: 'schedule'
description: 'Preview the behavior of a cron schedule.'
flags: [
cli.Flag{
name: 'count'
description: 'How many scheduled times to show.'
flag: cli.FlagType.int
default_value: ['5']
},
]
execute: fn (cmd cli.Command) ? {
ce := parse_expression(cmd.args.join(' '))?
count := cmd.flags.get_int('count')?
for t in ce.next_n(time.now(), count)? {
println(t)
}
}
}
}

View File

@ -7,9 +7,9 @@ import cron.expression { CronExpression, parse_expression }
import math
import build
import docker
import db
import os
import client
import models { GitRepo }
const (
// How many seconds to wait before retrying to update API if failed
@ -20,7 +20,7 @@ const (
struct ScheduledBuild {
pub:
repo db.GitRepo
repo GitRepo
timestamp time.Time
}
@ -38,7 +38,7 @@ mut:
api_update_frequency int
image_rebuild_frequency int
// Repos currently loaded from API.
repos []db.GitRepo
repos []GitRepo
// At what point to update the list of repositories.
api_update_timestamp time.Time
image_build_timestamp time.Time
@ -149,7 +149,7 @@ pub fn (mut d Daemon) run() {
}
// schedule_build adds the next occurence of the given repo build to the queue.
fn (mut d Daemon) schedule_build(repo db.GitRepo) {
fn (mut d Daemon) schedule_build(repo GitRepo) {
ce := if repo.schedule != '' {
parse_expression(repo.schedule) or {
// TODO This shouldn't return an error if the expression is empty.
@ -178,7 +178,7 @@ fn (mut d Daemon) schedule_build(repo db.GitRepo) {
fn (mut d Daemon) renew_repos() {
d.linfo('Renewing repos...')
mut new_repos := d.client.get_git_repos() or {
mut new_repos := d.client.get_all_git_repos() or {
d.lerror('Failed to renew repos. Retrying in ${daemon.api_update_retry_timeout}s...')
d.api_update_timestamp = time.now().add_seconds(daemon.api_update_retry_timeout)

View File

@ -121,6 +121,20 @@ pub fn (ce &CronExpression) next_from_now() ?time.Time {
return ce.next(time.now())
}
// next_n returns the n next occurences of the expression, given a starting
// time.
pub fn (ce &CronExpression) next_n(ref time.Time, n int) ?[]time.Time {
mut times := []time.Time{cap: n}
times << ce.next(ref)?
for i in 1 .. n {
times << ce.next(times[i - 1])?
}
return times
}
// parse_range parses a given string into a range of sorted integers, if
// possible.
fn parse_range(s string, min int, max int, mut bitv []bool) ? {

View File

@ -1,21 +1,89 @@
module db
import sqlite
import time
struct VieterDb {
conn sqlite.DB
}
struct MigrationVersion {
id int [primary]
version int
}
const (
migrations_up = [$embed_file('migrations/001-initial/up.sql')]
migrations_down = [$embed_file('migrations/001-initial/down.sql')]
)
// init initializes a database & adds the correct tables.
pub fn init(db_path string) ?VieterDb {
conn := sqlite.connect(db_path)?
sql conn {
create table GitRepo
create table BuildLog
create table MigrationVersion
}
cur_version := sql conn {
select from MigrationVersion limit 1
}
// If there's no row yet, we add it here
if cur_version == MigrationVersion{} {
sql conn {
insert cur_version into MigrationVersion
}
}
// Apply each migration in order
for i in cur_version.version .. db.migrations_up.len {
migration := db.migrations_up[i].to_string()
version_num := i + 1
// vfmt does not like these dots
println('Applying migration $version_num' + '...')
// The sqlite library seems to not like it when multiple statements are
// passed in a single exec. Therefore, we split them & run them all
// separately.
for part in migration.split(';').map(it.trim_space()).filter(it != '') {
res := conn.exec_none(part)
if res != sqlite.sqlite_done {
return error('An error occurred while applying migration $version_num')
}
}
// The where clause doesn't really matter, as there will always only be
// one entry anyways.
sql conn {
update MigrationVersion set version = version_num where id > 0
}
}
return VieterDb{
conn: conn
}
}
// row_into<T> converts an sqlite.Row into a given type T by parsing each field
// from a string according to its type.
pub fn row_into<T>(row sqlite.Row) T {
mut i := 0
mut out := T{}
$for field in T.fields {
$if field.typ is string {
out.$(field.name) = row.vals[i]
} $else $if field.typ is int {
out.$(field.name) = row.vals[i].int()
} $else $if field.typ is time.Time {
out.$(field.name) = time.unix(row.vals[i].int())
}
i += 1
}
return out
}

View File

@ -1,85 +1,21 @@
module db
pub struct GitRepoArch {
pub:
id int [primary; sql: serial]
repo_id int [nonull]
value string [nonull]
}
// str returns a string representation.
pub fn (gra &GitRepoArch) str() string {
return gra.value
}
pub struct GitRepo {
pub mut:
id int [optional; primary; sql: serial]
// URL of the Git repository
url string [nonull]
// Branch of the Git repository to use
branch string [nonull]
// Which repo the builder should publish packages to
repo string [nonull]
// Cron schedule describing how frequently to build the repo.
schedule string [optional]
// On which architectures the package is allowed to be built. In reality,
// this controls which builders will periodically build the image.
arch []GitRepoArch [fkey: 'repo_id']
}
// str returns a string representation.
pub fn (gr &GitRepo) str() string {
mut parts := [
'id: $gr.id',
'url: $gr.url',
'branch: $gr.branch',
'repo: $gr.repo',
'schedule: $gr.schedule',
'arch: ${gr.arch.map(it.value).join(', ')}',
]
str := parts.join('\n')
return str
}
// patch_from_params patches a GitRepo from a map[string]string, usually
// provided from a web.App's params
pub fn (mut r GitRepo) patch_from_params(params map[string]string) {
$for field in GitRepo.fields {
if field.name in params {
$if field.typ is string {
r.$(field.name) = params[field.name]
// This specific type check is needed for the compiler to ensure
// our types are correct
} $else $if field.typ is []GitRepoArch {
r.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it })
}
}
}
}
// git_repo_from_params creates a GitRepo from a map[string]string, usually
// provided from a web.App's params
pub fn git_repo_from_params(params map[string]string) ?GitRepo {
mut repo := GitRepo{}
// If we're creating a new GitRepo, we want all fields to be present before
// "patching".
$for field in GitRepo.fields {
if field.name !in params && !field.attrs.contains('optional') {
return error('Missing parameter: ${field.name}.')
}
}
repo.patch_from_params(params)
return repo
}
import models { GitRepo, GitRepoArch, GitRepoFilter }
// get_git_repos returns all GitRepo's in the database.
pub fn (db &VieterDb) get_git_repos() []GitRepo {
pub fn (db &VieterDb) get_git_repos(filter GitRepoFilter) []GitRepo {
// This seems to currently be blocked by a bug in the ORM, I'll have to ask
// around.
if filter.repo != '' {
res := sql db.conn {
select from GitRepo order by id
select from GitRepo where repo == filter.repo order by id limit filter.limit offset filter.offset
}
return res
}
res := sql db.conn {
select from GitRepo order by id limit filter.limit offset filter.offset
}
return res
@ -130,6 +66,7 @@ pub fn (db &VieterDb) update_git_repo(repo_id int, params map[string]string) {
}
}
values_str := values.join(', ')
// I think this is actual SQL & not the ORM language
query := 'update GitRepo set $values_str where id == $repo_id'
db.conn.exec_none(query)

View File

@ -1,38 +1,57 @@
module db
import models { BuildLog, BuildLogFilter }
import time
pub struct BuildLog {
pub:
id int [primary; sql: serial]
repo_id int [nonull]
start_time time.Time [nonull]
end_time time.Time [nonull]
arch string [nonull]
exit_code int [nonull]
}
// str returns a string representation.
pub fn (bl &BuildLog) str() string {
mut parts := [
'id: $bl.id',
'repo id: $bl.repo_id',
'start time: $bl.start_time',
'end time: $bl.end_time',
'arch: $bl.arch',
'exit code: $bl.exit_code',
]
str := parts.join('\n')
return str
}
// get_build_logs returns all BuildLog's in the database.
pub fn (db &VieterDb) get_build_logs() []BuildLog {
res := sql db.conn {
select from BuildLog order by id
pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog {
mut where_parts := []string{}
if filter.repo != 0 {
where_parts << 'repo_id == $filter.repo'
}
if filter.before != time.Time{} {
where_parts << 'start_time < $filter.before.unix_time()'
}
if filter.after != time.Time{} {
where_parts << 'start_time > $filter.after.unix_time()'
}
// NOTE: possible SQL injection
if filter.arch != '' {
where_parts << "arch == '$filter.arch'"
}
mut parts := []string{}
for exp in filter.exit_codes {
if exp[0] == `!` {
code := exp[1..].int()
parts << 'exit_code != $code'
} else {
code := exp.int()
parts << 'exit_code == $code'
}
}
if parts.len > 0 {
where_parts << parts.map('($it)').join(' or ')
}
mut where_str := ''
if where_parts.len > 0 {
where_str = 'where ' + where_parts.map('($it)').join(' and ')
}
query := 'select * from BuildLog $where_str limit $filter.limit offset $filter.offset'
rows, _ := db.conn.exec(query)
res := rows.map(row_into<BuildLog>(it))
return res
}

View File

@ -0,0 +1,3 @@
DROP TABLE IF EXISTS BuildLog;
DROP TABLE IF EXISTS GitRepoArch;
DROP TABLE IF EXISTS GitRepo;

View File

@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS GitRepo (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL,
branch TEXT NOT NULL,
repo TEXT NOT NULL,
schedule TEXT
);
CREATE TABLE IF NOT EXISTS GitRepoArch (
id INTEGER PRIMARY KEY,
repo_id INTEGER NOT NULL,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS BuildLog (
id INTEGER PRIMARY KEY,
repo_id INTEGER NOT NULL,
start_time INTEGER NOT NULL,
end_time iNTEGER NOT NULL,
arch TEXT NOT NULL,
exit_code INTEGER NOT NULL
);

View File

@ -83,6 +83,7 @@ pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect {
mut data := json.decode(ContainerInspect, body)?
// The Docker engine API *should* always return UTC time.
data.state.start_time = time.parse_rfc3339(data.state.start_time_str)?
if data.state.status == 'exited' {

View File

@ -5,13 +5,15 @@ import server
import cli
import console.git
import console.logs
import console.schedule
import console.man
import cron
fn main() {
mut app := cli.Command{
name: 'vieter'
description: 'Vieter is a lightweight implementation of an Arch repository server.'
version: '0.3.0-alpha.2'
version: '0.3.0-rc.1'
flags: [
cli.Flag{
flag: cli.FlagType.string
@ -27,6 +29,8 @@ fn main() {
git.cmd(),
cron.cmd(),
logs.cmd(),
schedule.cmd(),
man.cmd(),
]
}
app.setup()

52
src/models/git.v 100644
View File

@ -0,0 +1,52 @@
module models
pub struct GitRepoArch {
pub:
id int [primary; sql: serial]
repo_id int [nonull]
value string [nonull]
}
// str returns a string representation.
pub fn (gra &GitRepoArch) str() string {
return gra.value
}
pub struct GitRepo {
pub mut:
id int [primary; sql: serial]
// URL of the Git repository
url string [nonull]
// Branch of the Git repository to use
branch string [nonull]
// Which repo the builder should publish packages to
repo string [nonull]
// Cron schedule describing how frequently to build the repo.
schedule string
// On which architectures the package is allowed to be built. In reality,
// this controls which builders will periodically build the image.
arch []GitRepoArch [fkey: 'repo_id']
}
// str returns a string representation.
pub fn (gr &GitRepo) str() string {
mut parts := [
'id: $gr.id',
'url: $gr.url',
'branch: $gr.branch',
'repo: $gr.repo',
'schedule: $gr.schedule',
'arch: ${gr.arch.map(it.value).join(', ')}',
]
str := parts.join('\n')
return str
}
[params]
pub struct GitRepoFilter {
pub mut:
limit u64 = 25
offset u64
repo string
}

41
src/models/logs.v 100644
View File

@ -0,0 +1,41 @@
module models
import time
pub struct BuildLog {
pub mut:
id int [primary; sql: serial]
repo_id int [nonull]
start_time time.Time [nonull]
end_time time.Time [nonull]
arch string [nonull]
exit_code int [nonull]
}
// str returns a string representation.
pub fn (bl &BuildLog) str() string {
mut parts := [
'id: $bl.id',
'repo id: $bl.repo_id',
'start time: $bl.start_time.local()',
'end time: $bl.end_time.local()',
'duration: ${bl.end_time - bl.start_time}',
'arch: $bl.arch',
'exit code: $bl.exit_code',
]
str := parts.join('\n')
return str
}
[params]
pub struct BuildLogFilter {
pub mut:
limit u64 = 25
offset u64
repo int
before time.Time
after time.Time
arch string
exit_codes []string
}

View File

@ -0,0 +1,53 @@
module models
import time
// from_params<T> creates a new instance of T from the given map by parsing all
// of its fields from the map.
pub fn from_params<T>(params map[string]string) ?T {
mut o := T{}
patch_from_params<T>(mut o, params)?
return o
}
// patch_from_params<T> updates the given T object with the params defined in
// the map.
pub fn patch_from_params<T>(mut o T, params map[string]string) ? {
$for field in T.fields {
if field.name in params && params[field.name] != '' {
$if field.typ is string {
o.$(field.name) = params[field.name]
} $else $if field.typ is int {
o.$(field.name) = params[field.name].int()
} $else $if field.typ is u64 {
o.$(field.name) = params[field.name].u64()
} $else $if field.typ is []GitRepoArch {
o.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it })
} $else $if field.typ is time.Time {
o.$(field.name) = time.unix(params[field.name].int())
} $else $if field.typ is []string {
o.$(field.name) = params[field.name].split(',')
}
} else if field.attrs.contains('nonull') {
return error('Missing parameter: ${field.name}.')
}
}
}
// params_from<T> converts a given T struct into a map of strings.
pub fn params_from<T>(o &T) map[string]string {
mut out := map[string]string{}
$for field in T.fields {
$if field.typ is time.Time {
out[field.name] = o.$(field.name).unix_time().str()
} $else $if field.typ is []string {
out[field.name] = o.$(field.name).join(',')
} $else {
out[field.name] = o.$(field.name).str()
}
}
return out
}

View File

@ -4,6 +4,7 @@ import web
import net.http
import response { new_data_response, new_response }
import db
import models { GitRepo, GitRepoArch, GitRepoFilter }
// get_repos returns the current list of repos.
['/api/repos'; get]
@ -12,7 +13,10 @@ fn (mut app App) get_repos() web.Result {
return app.json(http.Status.unauthorized, new_response('Unauthorized.'))
}
repos := app.db.get_git_repos()
filter := models.from_params<GitRepoFilter>(app.query) or {
return app.json(http.Status.bad_request, new_response('Invalid query parameters.'))
}
repos := app.db.get_git_repos(filter)
return app.json(http.Status.ok, new_data_response(repos))
}
@ -44,7 +48,7 @@ fn (mut app App) post_repo() web.Result {
params['arch'] = app.conf.default_arch
}
new_repo := db.git_repo_from_params(params) or {
new_repo := models.from_params<GitRepo>(params) or {
return app.json(http.Status.bad_request, new_response(err.msg()))
}
@ -75,7 +79,7 @@ fn (mut app App) patch_repo(id int) web.Result {
app.db.update_git_repo(id, app.query)
if 'arch' in app.query {
arch_objs := app.query['arch'].split(',').map(db.GitRepoArch{ value: it })
arch_objs := app.query['arch'].split(',').map(GitRepoArch{ value: it })
app.db.update_git_repo_archs(id, arch_objs)
}

View File

@ -8,6 +8,7 @@ import db
import time
import os
import util
import models { BuildLog, BuildLogFilter }
// get_logs returns all build logs in the database. A 'repo' query param can
// optionally be added to limit the list of build logs to that repository.
@ -17,11 +18,10 @@ fn (mut app App) get_logs() web.Result {
return app.json(http.Status.unauthorized, new_response('Unauthorized.'))
}
logs := if 'repo' in app.query {
app.db.get_build_logs_for_repo(app.query['repo'].int())
} else {
app.db.get_build_logs()
filter := models.from_params<BuildLogFilter>(app.query) or {
return app.json(http.Status.bad_request, new_response('Invalid query parameters.'))
}
logs := app.db.get_build_logs(filter)
return app.json(http.Status.ok, new_data_response(logs))
}
@ -70,13 +70,19 @@ fn (mut app App) post_log() web.Result {
}
// Parse query params
start_time := parse_query_time(app.query['startTime']) or {
start_time_int := app.query['startTime'].int()
if start_time_int == 0 {
return app.json(http.Status.bad_request, new_response('Invalid or missing start time.'))
}
start_time := time.unix(start_time_int)
end_time := parse_query_time(app.query['endTime']) or {
end_time_int := app.query['endTime'].int()
if end_time_int == 0 {
return app.json(http.Status.bad_request, new_response('Invalid or missing end time.'))
}
end_time := time.unix(end_time_int)
if 'exitCode' !in app.query {
return app.json(http.Status.bad_request, new_response('Missing exit code.'))
@ -97,7 +103,7 @@ fn (mut app App) post_log() web.Result {
}
// Store log in db
log := db.BuildLog{
log := BuildLog{
repo_id: repo_id
start_time: start_time
end_time: end_time

View File

@ -68,7 +68,9 @@ pub fn server(conf Config) ? {
}
db_file := os.join_path_single(conf.data_dir, server.db_file_name)
db := db.init(db_file) or { util.exit_with_message(1, 'Failed to initialize database.') }
db := db.init(db_file) or {
util.exit_with_message(1, 'Failed to initialize database: $err.msg()')
}
web.run(&App{
logger: logger

View File

@ -25,12 +25,12 @@ pub const (
http_302 = http.new_response(
status: .found
text: '302 Found'
body: '302 Found'
header: headers_close
)
http_400 = http.new_response(
status: .bad_request
text: '400 Bad Request'
body: '400 Bad Request'
header: http.new_header(
key: .content_type
value: 'text/plain'
@ -38,7 +38,7 @@ pub const (
)
http_404 = http.new_response(
status: .not_found
text: '404 Not Found'
body: '404 Not Found'
header: http.new_header(
key: .content_type
value: 'text/plain'
@ -46,7 +46,7 @@ pub const (
)
http_500 = http.new_response(
status: .internal_server_error
text: '500 Internal Server Error'
body: '500 Internal Server Error'
header: http.new_header(
key: .content_type
value: 'text/plain'
@ -209,7 +209,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo
mut resp := http.Response{
header: header.join(web.headers_close)
text: res
body: res
}
resp.set_version(.v1_1)
resp.set_status(ctx.status)