Live video on the AT Protocol
1package main
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/json"
7 "flag"
8 "fmt"
9 "io"
10 "math/rand"
11 "net/http"
12 "os"
13 "sort"
14 "strings"
15 "text/template"
16
17 "github.com/go-git/go-git/v5"
18 "github.com/go-git/go-git/v5/plumbing"
19 "github.com/go-git/go-git/v5/plumbing/object"
20 "github.com/go-git/go-git/v5/plumbing/storer"
21 "github.com/google/uuid"
22)
23
24func main() {
25 err := makeGit()
26 if err != nil {
27 panic(err)
28 }
29}
30
31var tmpl = `package main
32
33var Version = "%s"
34var BuildTime = "%d"
35var UUID = "%s"
36`
37
38var tmplJS = `
39export const version = "%s";
40export const buildTime = "%d";
41export const uuid = "%s";
42`
43
44func gitlabURL() string {
45 CI_API_V4_URL := os.Getenv("CI_API_V4_URL") //nolint:all
46 CIProjectID := os.Getenv("CI_PROJECT_ID")
47 CI_API_V4_URL = strings.Replace(CI_API_V4_URL, "https://git.stream.place", "https://git-cloudflare.stream.place", 1)
48 return fmt.Sprintf("%s/projects/%s", CI_API_V4_URL, CIProjectID)
49}
50
51func gitlab(suffix string, dest any) {
52 u := fmt.Sprintf("%s%s", gitlabURL(), suffix)
53
54 req, err := http.Get(u)
55 if err != nil {
56 panic(err)
57 }
58 if err := json.NewDecoder(req.Body).Decode(dest); err != nil {
59 panic(err)
60 }
61}
62
63func gitlabList(suffix string) []map[string]any {
64 var result []map[string]any
65 gitlab(suffix, &result)
66 return result
67}
68
69func gitlabDict(suffix string) map[string]any {
70 var result map[string]any
71 gitlab(suffix, &result)
72 return result
73}
74
75func makeGit() error {
76 output := flag.String("o", "", "file to output to")
77 version := flag.Bool("v", false, "just print version")
78 env := flag.Bool("env", false, "print a bunch of useful environment variables")
79 doBranch := flag.Bool("branch", false, "print branch")
80 doRelease := flag.Bool("release", false, "print release json file")
81 javascript := flag.Bool("js", false, "print code in javascript format")
82 homebrew := flag.Bool("homebrew", false, "print homebrew formula")
83
84 flag.Parse()
85
86 // handle CF_PAGES environment fallback
87 if os.Getenv("CF_PAGES") != "" && *javascript {
88 out := `export const version = "unknown"; export const buildTime = 0; export const uuid = "00000000-0000-0000-0000-000000000000";`
89 if *output != "" {
90 if err := os.WriteFile(*output, []byte(out), 0644); err != nil {
91 return err
92 }
93 } else {
94 fmt.Print(out)
95 }
96 return nil
97 }
98 r, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
99 if err != nil {
100 return err
101 }
102
103 // ... retrieving the HEAD reference
104 ref, err := r.Head()
105 if err != nil {
106 return err
107 }
108 c, err := r.CommitObject(ref.Hash())
109 if err != nil {
110 return err
111 }
112
113 ts := c.Author.When.Unix()
114 rander := rand.New(rand.NewSource(ts))
115 u, err := uuid.NewV7FromReader(rander)
116 if err != nil {
117 return err
118 }
119 g, err := PlainOpen(".")
120 if err != nil {
121 return err
122 }
123 desc, err := g.Describe(ref)
124 if err != nil {
125 return err
126 }
127 var out string
128 if *version {
129 out = desc
130 } else if *doBranch {
131 out = branch()
132 } else if *env {
133 StreamplaceBranch := branch()
134 outMap := map[string]string{}
135 outMap["STREAMPLACE_BRANCH"] = StreamplaceBranch
136 outMap["STREAMPLACE_VERSION"] = desc
137 outMap["STREAMPLACE_BRANCH"] = StreamplaceBranch
138 for _, arch := range []string{"amd64", "arm64"} {
139 k := fmt.Sprintf("STREAMPLACE_URL_LINUX_%s", strings.ToUpper(arch))
140 v := fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-%s-linux-%s.tar.gz", gitlabURL(), StreamplaceBranch, desc, desc, arch)
141 outMap[k] = v
142 macK := fmt.Sprintf("STREAMPLACE_URL_DARWIN_%s", strings.ToUpper(arch))
143 macV := fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-%s-darwin-%s.zip", gitlabURL(), StreamplaceBranch, desc, desc, arch)
144 outMap[macK] = macV
145 }
146 outMap["STREAMPLACE_DESKTOP_URL_WINDOWS_AMD64"] = fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-desktop-%s-windows-amd64.exe", gitlabURL(), StreamplaceBranch, desc, desc)
147 for k, v := range outMap {
148 out = out + fmt.Sprintf("%s=%s\n", k, v)
149 }
150 } else if *doRelease {
151 outMap := map[string]any{}
152 outMap["name"] = desc
153 outMap["tag-name"] = desc
154 pkgs := gitlabList(fmt.Sprintf("/packages?order_by=created_at&sort=desc&package_name=%s", branch()))
155 id := pkgs[0]["id"].(float64)
156 pkgFiles := gitlabList(fmt.Sprintf("/packages/%d/package_files", int(id)))
157 outFiles := []string{}
158 sort.Slice(pkgFiles, func(i, j int) bool {
159 s1 := pkgFiles[i]["file_name"].(string)
160 s2 := pkgFiles[j]["file_name"].(string)
161 return s1 < s2
162 })
163 for _, file := range pkgFiles {
164 fileJSON := map[string]string{
165 "name": file["file_name"].(string),
166 "url": fmt.Sprintf("%s/packages/generic/%s/%s/%s", gitlabURL(), branch(), desc, file["file_name"].(string)),
167 }
168 bs, err := json.Marshal(fileJSON)
169 if err != nil {
170 return err
171 }
172 outFiles = append(outFiles, string(bs))
173 }
174 outMap["assets-link"] = outFiles
175 changelog := gitlabDict(fmt.Sprintf("/repository/changelog?version=%s", desc))
176 outMap["description"] = changelog["notes"]
177 bs, err := json.MarshalIndent(outMap, "", " ")
178 if err != nil {
179 return err
180 }
181 out = string(bs)
182 } else if *javascript {
183 out = fmt.Sprintf(tmplJS, desc, ts, u)
184 } else if *homebrew {
185 bs := bytes.Buffer{}
186 versionNoV := strings.TrimPrefix(desc, "v")
187 darwinAmd64File := fmt.Sprintf("streamplace-%s-darwin-amd64.tar.gz", desc)
188 darwinArm64File := fmt.Sprintf("streamplace-%s-darwin-arm64.tar.gz", desc)
189 linuxAmd64File := fmt.Sprintf("streamplace-%s-linux-amd64.tar.gz", desc)
190 linuxArm64File := fmt.Sprintf("streamplace-%s-linux-arm64.tar.gz", desc)
191
192 err = homebrewTmpl.Execute(&bs, Homebrew{
193 Version: versionNoV,
194 DarwinArm64: getHash(darwinArm64File),
195 DarwinAmd64: getHash(darwinAmd64File),
196 LinuxArm64: getHash(linuxArm64File),
197 LinuxAmd64: getHash(linuxAmd64File),
198 })
199 if err != nil {
200 return err
201 }
202 out = bs.String()
203 } else {
204 out = fmt.Sprintf(tmpl, desc, ts, u)
205 }
206
207 if *output != "" {
208 if err := os.WriteFile(*output, []byte(out), 0644); err != nil {
209 return err
210 }
211 } else {
212 fmt.Print(out)
213 }
214 return nil
215}
216
217func getHash(fileName string) string {
218 filePath := fmt.Sprintf("bin/%s", fileName)
219 f, err := os.Open(filePath)
220 if err != nil {
221 panic(err)
222 }
223 defer f.Close()
224
225 h := sha256.New()
226 buf := make([]byte, 1024*1024) // 1MB buffer
227
228 for {
229 n, err := f.Read(buf)
230 if n > 0 {
231 if _, err := h.Write(buf[:n]); err != nil {
232 panic(err)
233 }
234 }
235 if err != nil {
236 if err == io.EOF {
237 break
238 }
239 panic(err)
240 }
241 }
242
243 return fmt.Sprintf("%x", h.Sum(nil))
244}
245
246func branch() string {
247 CICommitTag := os.Getenv("CI_COMMIT_TAG")
248 CICommitBranch := os.Getenv("CI_COMMIT_BRANCH")
249 if CICommitTag != "" {
250 return "latest"
251 } else if CICommitBranch != "" {
252 return strings.ReplaceAll(CICommitBranch, "/", "-")
253 } else {
254 panic("CI_COMMIT_TAG and CI_COMMIT_BRANCH undefined, can't get branch")
255 }
256}
257
258// Git struct wrapps Repository class from go-git to add a tag map used to perform queries when describing.
259type Git struct {
260 TagsMap map[plumbing.Hash]*plumbing.Reference
261 *git.Repository
262}
263
264// PlainOpen opens a git repository from the given path. It detects if the
265// repository is bare or a normal one. If the path doesn't contain a valid
266// repository ErrRepositoryNotExists is returned
267func PlainOpen(path string) (*Git, error) {
268 r, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true})
269 return &Git{
270 make(map[plumbing.Hash]*plumbing.Reference),
271 r,
272 }, err
273}
274
275func (g *Git) getTagMap() error {
276 tags, err := g.Tags()
277 if err != nil {
278 return err
279 }
280
281 err = tags.ForEach(func(t *plumbing.Reference) error {
282 h, err := g.ResolveRevision(plumbing.Revision(t.Name()))
283 if err != nil {
284 return err
285 }
286 g.TagsMap[*h] = t
287 return nil
288 })
289
290 return err
291}
292
293// Describe the reference as 'git describe --tags' will do
294func (g *Git) Describe(reference *plumbing.Reference) (string, error) {
295 if os.Getenv("STREAMPLACE_VERSION_OVERRIDE") != "" {
296 return os.Getenv("STREAMPLACE_VERSION_OVERRIDE"), nil
297 }
298
299 // Fetch the reference log
300 cIter, err := g.Log(&git.LogOptions{
301 // From: reference.Hash(),
302 Order: git.LogOrderCommitterTime,
303 })
304 if err != nil {
305 return "", err
306 }
307
308 // Build the tag map
309 err = g.getTagMap()
310 if err != nil {
311 return "", err
312 }
313
314 // Search the tag
315 var tag *plumbing.Reference
316 var count int
317 err = cIter.ForEach(func(c *object.Commit) error {
318 t, ok := g.TagsMap[c.Hash]
319 if ok {
320 tag = t
321 return storer.ErrStop
322 }
323 count++
324 return nil
325 })
326 if err != nil {
327 return "", err
328 }
329 head, err := g.Head()
330 if err != nil {
331 return "", err
332 }
333 if count == 0 && os.Getenv("CI_COMMIT_TAG") != "" {
334 return fmt.Sprint(tag.Name().Short()), nil
335 } else {
336 return fmt.Sprintf("%s-%s",
337 tag.Name().Short(),
338 head.Hash().String()[0:8],
339 ), nil
340 }
341}
342
343type Homebrew struct {
344 Version string
345 DarwinArm64 string
346 DarwinAmd64 string
347 LinuxArm64 string
348 LinuxAmd64 string
349}
350
351var homebrewTmpl = template.Must(template.New("homebrew").Parse(`
352class Streamplace < Formula
353 desc "Live video for the AT Protocol. Solving video for everybody forever."
354 homepage "https://stream.place"
355 license "GPL-3.0-or-later"
356 version "{{.Version}}"
357
358 on_macos do
359 if Hardware::CPU.arm?
360 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-darwin-arm64.tar.gz"
361 sha256 "{{.DarwinArm64}}"
362 end
363
364 if Hardware::CPU.intel?
365 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-darwin-amd64.tar.gz"
366 sha256 "{{.DarwinAmd64}}"
367 end
368 end
369
370 on_linux do
371 if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
372 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-linux-arm64.tar.gz"
373 sha256 "{{.LinuxArm64}}"
374 end
375
376 if Hardware::CPU.intel?
377 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-linux-amd64.tar.gz"
378 sha256 "{{.LinuxAmd64}}"
379 end
380 end
381
382 def install
383 bin.install "streamplace" => "streamplace"
384 end
385end
386`))