├── .gitignore ├── Procfile ├── Dockerfile.1 ├── Dockerfile ├── repo.json ├── handout-mode.patch ├── Makefile ├── package.json ├── README.md ├── .circleci └── config.yml ├── default.nix └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /Dockerfile.1: -------------------------------------------------------------------------------- 1 | FROM terrorjack/vanilla:circleci 2 | 3 | RUN apk add xz 4 | 5 | COPY . /root/workspace 6 | RUN cd /root/workspace && nix-shell --run "echo ok" 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dramforever/os-lectures-build:texlive 2 | 3 | RUN rm -rf /root/workspace 4 | COPY . /root/workspace 5 | RUN cd /root/workspace && nix-shell --run "echo ok" 6 | -------------------------------------------------------------------------------- /repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://github.com/LearningOS/os-lectures.git", 3 | "rev": "c8d771c861c15a025f51f5a808d05fce56015df8", 4 | "date": "2020-02-17T00:38:30+08:00", 5 | "sha256": "1wmfkbgfyz51z9abkkd0z4ka2gjlahqy0d1imrxg0wfggsran78i", 6 | "fetchSubmodules": true 7 | } 8 | -------------------------------------------------------------------------------- /handout-mode.patch: -------------------------------------------------------------------------------- 1 | diff --git a/preamble.tex b/preamble.tex 2 | index c8fe2f9..e8918ef 100644 3 | --- a/preamble.tex 4 | +++ b/preamble.tex 5 | @@ -15,7 +15,7 @@ 6 | % PACKAGES AND THEMES 7 | %---------------------------------------------------------------------------------------- 8 | 9 | -\documentclass[UTF8,aspectratio=169,12pt]{ctexbeamer} 10 | +\documentclass[UTF8,aspectratio=169,12pt,handout]{ctexbeamer} 11 | 12 | \usepackage{hyperref} 13 | \hypersetup{ 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE = $(filter-out slide-template.tex,$(wildcard *.tex)) 2 | PDF = $(patsubst %.tex,%.pdf,$(SOURCE)) 3 | NAME = $(notdir $(CURDIR)) 4 | 5 | $(NAME).pdf: $(sort $(PDF)) 6 | pdftk $^ cat output $@ 7 | 8 | %.pdf: %.tex 9 | latexmk -pdf -pdflatex='xelatex %O %S' $< 10 | 11 | .PHONY: all clean cleanall 12 | all: $(NAME).pdf $(PDF) 13 | 14 | cleanall: 15 | rm -f *.pdf *.out *.toc *.aux *.log *.nav *.gz *.snm *.vrb *.bak *.org ~* 16 | 17 | clean: 18 | rm -f *.out *.toc *.aux *.log *.nav *.gz *.snm *.vrb 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "os-lectures-build-bot", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/dramforever/os-lectures-build.git" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "bugs": { 14 | "url": "https://github.com/dramforever/os-lectures-build/issues" 15 | }, 16 | "homepage": "https://github.com/dramforever/os-lectures-build#readme", 17 | "dependencies": { 18 | "pg": "^7.18.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # os-lectures-build 2 | 3 | https://github.com/LearningOS/os-lectures 讲义的自动构建 4 | 5 | ## 下载 6 | 7 | https://github.com/dramforever/os-lectures-build/releases 8 | 9 | ## 包含组件 10 | 11 | - 构建所用的 Nix 表达式 `default.nix` 及依赖的文件 `repo.json`、`handout-mode.patch`、`Makefile` 12 | - 完整描述了整个构建过程,你可能看不懂 = =,但本质和原 repo 是一样的 13 | - 用的是 TeX Live 2019 14 | - `repo.json`:用 `nix-prefech-git` 生成的锁定版本的仓库信息 15 | ```console 16 | $ nix-prefetch-git --url "https://github.com/LearningOS/os-lectures.git" > repo.json 17 | ``` 18 | - `handout-mode.patch`:使用 Beamer 的 handout 模式,关闭“动画”。 19 | - `Makefile`:使用 `latexmk` 自动处理构建时要求运行 `xelated` 多次的情况,使用 `pdftk` 将 PDF 文件合并 20 | - `Dockerfile` 和 `Dockerfile.1` 缓存构建依赖 21 | - `.circleci/config.yml` CircleCI 配置文件,调用构建,上传 release 文件。 22 | - `index.js`、`package.json`、`Procfile`:自动更新 Bot,白嫖 Heroku 23 | - 向 `/update/$BOT_SECRET` 发送任意 `POST` 请求触发一次更新,自动检查是否最新 commit 变动,若有在 CircleCI 触发一次 build 24 | - 环境变量: 25 | - `BOT_SECRET` 字符串,用法如前所述 26 | - `DATABASE_URL` 字符串,PostgreSQL 数据库 connection string 27 | - `CIRCLECI_TOKEN` 字符串 28 | - `GITHUB_TOKEN` 字符串,可选 29 | 30 | ## TODO 31 | 32 | - PDF 目录 33 | - 有些 hard-coded 的部分可以改进 34 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: dramforever/os-lectures-build 7 | steps: 8 | - checkout 9 | - run: 10 | name: Update repo.json 11 | command: | 12 | /root/.nix-profile/bin/nix run nixpkgs.nix-prefetch-git -c \ 13 | nix-prefetch-git --url "https://github.com/LearningOS/os-lectures.git" > repo.json 14 | 15 | - run: 16 | name: Build 17 | command: | 18 | /root/.nix-profile/bin/nix-build 19 | 20 | - run: 21 | name: Copy artifacts 22 | command: cp -rH result output 23 | 24 | - run: 25 | name: Release 26 | command: | 27 | apk add hub jq 28 | 29 | files="" 30 | 31 | for i in output/*.pdf; do 32 | files="$files -a $i" 33 | done 34 | 35 | cat > release.txt < 0 ] && echo "Failed slides") 42 | 43 | $(cat output/failed) 44 | END 45 | 46 | hub release create $files -F release.txt "v$CIRCLE_BUILD_NUM" 47 | 48 | - store_artifacts: 49 | path: output 50 | desination: / 51 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = fetchTarball { 3 | url = "https://releases.nixos.org/nixpkgs/nixpkgs-20.09pre213040.f77e057cda6/nixexprs.tar.xz"; 4 | sha256 = "1khcsagyah8j7kmmmysvm1bprszp7ivvp0q0jvgy3kgcnxfs3pr9"; 5 | }; 6 | 7 | in (import nixpkgs {}).callPackage ( 8 | { stdenvNoCC, fetchgit, lib, makeFontsConf 9 | , noto-fonts, noto-fonts-extra 10 | , texlive, fontconfig, pdftk }: 11 | 12 | let noto-fonts-cjk-ttc = stdenvNoCC.mkDerivation { 13 | name = "noto-fonts-cjk-ttc-2.001"; 14 | src = fetchgit { 15 | url = "https://github.com/googlefonts/noto-cjk.git"; 16 | rev = "be6c059ac1587e556e2412b27f5155c8eb3ddbe6"; 17 | sha256 = "0p6mhpg89f9zc4vpi42pn2jm900hs44ns0p2kh6jcs1a2p9ma69w"; 18 | }; 19 | 20 | phases = [ "unpackPhase" "installPhase" ]; 21 | 22 | installPhase = '' 23 | mkdir -p "$out/share/fonts/noto-cjk" 24 | cp *.ttc "$out/share/fonts/noto-cjk" 25 | ''; 26 | }; 27 | 28 | in stdenvNoCC.mkDerivation { 29 | name = "os-lectures-0"; 30 | 31 | src = fetchgit { 32 | inherit (builtins.fromJSON (lib.readFile ./repo.json)) 33 | url rev sha256 fetchSubmodules; 34 | }; 35 | 36 | nativeBuildInputs = [ 37 | texlive.combined.scheme-full 38 | fontconfig 39 | pdftk 40 | ]; 41 | 42 | FONTCONFIG_FILE = makeFontsConf { 43 | fontDirectories = [ 44 | noto-fonts 45 | noto-fonts-cjk-ttc 46 | noto-fonts-extra 47 | ]; 48 | }; 49 | 50 | phases = [ "unpackPhase" "patchPhase" "buildPhase" ]; 51 | 52 | patches = [ ./handout-mode.patch ]; 53 | 54 | buildPhase = '' 55 | shopt -s nullglob 56 | mkdir -p "$out/all_pdfs" "$out/logs" 57 | 58 | touch "$out/failed" 59 | 60 | for lec in lecture*; do 61 | if make -k -f "${./Makefile}" -C "$lec"; then 62 | cp "$lec/$lec".pdf "$out" 63 | else 64 | echo "- $lec" >> "$out/failed" 65 | fi 66 | 67 | for pdf in "$lec/"*.pdf; do 68 | cp "$pdf" "$out/all_pdfs" 69 | done 70 | 71 | for log in "$lec/"*.log; do 72 | cp "$log" "$out/logs" 73 | done 74 | done 75 | ''; 76 | }) {} 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const url = require('url'); 4 | const pg = require('pg'); 5 | 6 | // Perform an HTTPS request 7 | // 8 | // reqUrl: string of url 9 | // body: Unspecified or NULL for GET, specify for POST 10 | // headers: Object of header 11 | // 12 | // Returns: A Promise, resolving to an Object of: 13 | // status: The status code 14 | // headers: Object of headers 15 | // body: string of response 16 | function request(reqUrl, body = null, headers = null) { 17 | if (headers === null) { 18 | headers = {}; 19 | } 20 | 21 | if (body !== null) { 22 | body = Buffer.from(body); 23 | headers['Content-Length'] = body.length; 24 | } 25 | 26 | const options = Object.assign({}, url.parse(reqUrl), { 27 | method: body === null ? 'GET' : 'POST', 28 | headers 29 | }); 30 | 31 | return new Promise((resolve, reject) => { 32 | const req = https.request( 33 | options, 34 | (res) => { 35 | const chunks = []; 36 | let len = 0; 37 | 38 | res.on('data', (chunk) => { 39 | chunks.push(chunk); 40 | len += chunk.length; 41 | }); 42 | 43 | res.on('end', () => { 44 | const body = Buffer.concat(chunks, len).toString(); 45 | resolve({ 46 | status: res.statusCode, 47 | headers: res.headers, 48 | body 49 | }); 50 | }); 51 | 52 | res.on('error', (e) => { reject(e); }); 53 | }); 54 | 55 | req.on('error', (e) => { reject(e); }); 56 | 57 | if (body !== null) { 58 | req.write(body); 59 | } 60 | 61 | req.end(); 62 | }); 63 | } 64 | 65 | // Optionally use token for GitHub, depending on env GITHUB_TOKEN 66 | // Returns: An Object of headers, 67 | function githubHeader() { 68 | if (process.env.GITHUB_TOKEN) { 69 | return { 70 | 'Authorization': `token ${process.env.GITHUB_TOKEN}` 71 | }; 72 | } else { 73 | return {} 74 | } 75 | } 76 | 77 | async function doUpdate(client) { 78 | const readData = async () => { 79 | const res = await client.query('select data from bot_state limit 1'); 80 | 81 | if (res.rows.length > 0) 82 | return JSON.parse(res.rows[0].data); 83 | else 84 | return null; 85 | }; 86 | 87 | const writeData = async (data) => { 88 | await client.query('BEGIN'); 89 | 90 | await client.query('delete from bot_state'); 91 | await client.query('insert into bot_state (data) values ($1)', [ JSON.stringify(data) ]); 92 | 93 | await client.query('COMMIT'); 94 | }; 95 | 96 | const state = await readData(); 97 | 98 | const oldsha = state === null ? null : state.sha; 99 | const etagHeader = state === null ? {} : { 'If-None-Match': state.etag }; 100 | 101 | const github = await request( 102 | 'https://api.github.com/repos/LearningOS/os-lectures/branches/master', 103 | null, 104 | { 105 | 'User-Agent': 'dramforever', 106 | ... githubHeader(), 107 | ... etagHeader 108 | }); 109 | 110 | if (github.status === 304) 111 | return { sha: oldsha, updated: false }; 112 | 113 | const newsha = JSON.parse(github.body).commit.sha; 114 | 115 | if (oldsha === newsha) 116 | return { sha: oldsha, updated: false }; 117 | 118 | request( 119 | `https://circleci.com/api/v1.1/project/github/dramforever/os-lectures-build?circle-token=${process.env.CIRCLECI_TOKEN}`, 120 | '{}' 121 | ); 122 | 123 | await writeData({ 124 | sha: newsha, 125 | etag: github.headers.etag 126 | }); 127 | 128 | return { sha: newsha, updated: true }; 129 | } 130 | 131 | async function handle(request) { 132 | const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); 133 | try { 134 | await client.connect(); 135 | 136 | if (request.url === `/update/${process.env.BOT_SECRET}` 137 | && request.method === 'POST') { 138 | const result = await doUpdate(client); 139 | return result; 140 | } else { 141 | throw 'Bad request' 142 | } 143 | } finally { 144 | await client.end(); 145 | } 146 | } 147 | 148 | function topLevel(request, response) { 149 | handle(request) 150 | .then((res) => { 151 | if (typeof res === 'object') { 152 | response.writeHead(200, { 'Content-Type': 'application/json' }); 153 | response.end(JSON.stringify({ 154 | status: 'success', 155 | ... res 156 | })); 157 | } 158 | }) 159 | .catch((err) => { 160 | console.log(err); 161 | response.writeHead(400, { 'Content-Type': 'text/plain' }); 162 | response.end(JSON.stringify({ 163 | status: 'error', 164 | error: err.toString() 165 | })); 166 | }); 167 | } 168 | 169 | async function setupDatabase() { 170 | const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); 171 | try { 172 | await client.connect(); 173 | 174 | await client.query(` 175 | create table if not exists bot_state ( 176 | data text not null 177 | );`); 178 | } finally { 179 | await client.end(); 180 | } 181 | } 182 | 183 | setupDatabase() 184 | .then(() => { 185 | const server = http.createServer(topLevel); 186 | const port = process.env.PORT || 5000; 187 | server.listen(port); 188 | }) 189 | .catch(err => { 190 | console.error(err); 191 | }) 192 | --------------------------------------------------------------------------------