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 r, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
86 if err != nil {
87 return err
88 }
89
90 // ... retrieving the HEAD reference
91 ref, err := r.Head()
92 if err != nil {
93 return err
94 }
95 c, err := r.CommitObject(ref.Hash())
96 if err != nil {
97 return err
98 }
99
100 ts := c.Author.When.Unix()
101 rander := rand.New(rand.NewSource(ts))
102 u, err := uuid.NewV7FromReader(rander)
103 if err != nil {
104 return err
105 }
106 g, err := PlainOpen(".")
107 if err != nil {
108 return err
109 }
110 desc, err := g.Describe(ref)
111 if err != nil {
112 return err
113 }
114 var out string
115 if *version {
116 out = desc
117 } else if *doBranch {
118 out = branch()
119 } else if *env {
120 StreamplaceBranch := branch()
121 outMap := map[string]string{}
122 outMap["STREAMPLACE_BRANCH"] = StreamplaceBranch
123 outMap["STREAMPLACE_VERSION"] = desc
124 outMap["STREAMPLACE_BRANCH"] = StreamplaceBranch
125 for _, arch := range []string{"amd64", "arm64"} {
126 k := fmt.Sprintf("STREAMPLACE_URL_LINUX_%s", strings.ToUpper(arch))
127 v := fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-%s-linux-%s.tar.gz", gitlabURL(), StreamplaceBranch, desc, desc, arch)
128 outMap[k] = v
129 macK := fmt.Sprintf("STREAMPLACE_URL_DARWIN_%s", strings.ToUpper(arch))
130 macV := fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-%s-darwin-%s.zip", gitlabURL(), StreamplaceBranch, desc, desc, arch)
131 outMap[macK] = macV
132 }
133 outMap["STREAMPLACE_DESKTOP_URL_WINDOWS_AMD64"] = fmt.Sprintf("%s/packages/generic/%s/%s/streamplace-desktop-%s-windows-amd64.exe", gitlabURL(), StreamplaceBranch, desc, desc)
134 for k, v := range outMap {
135 out = out + fmt.Sprintf("%s=%s\n", k, v)
136 }
137 } else if *doRelease {
138 outMap := map[string]any{}
139 outMap["name"] = desc
140 outMap["tag-name"] = desc
141 pkgs := gitlabList(fmt.Sprintf("/packages?order_by=created_at&sort=desc&package_name=%s", branch()))
142 id := pkgs[0]["id"].(float64)
143 pkgFiles := gitlabList(fmt.Sprintf("/packages/%d/package_files", int(id)))
144 outFiles := []string{}
145 sort.Slice(pkgFiles, func(i, j int) bool {
146 s1 := pkgFiles[i]["file_name"].(string)
147 s2 := pkgFiles[j]["file_name"].(string)
148 return s1 < s2
149 })
150 for _, file := range pkgFiles {
151 fileJSON := map[string]string{
152 "name": file["file_name"].(string),
153 "url": fmt.Sprintf("%s/packages/generic/%s/%s/%s", gitlabURL(), branch(), desc, file["file_name"].(string)),
154 }
155 bs, err := json.Marshal(fileJSON)
156 if err != nil {
157 return err
158 }
159 outFiles = append(outFiles, string(bs))
160 }
161 outMap["assets-link"] = outFiles
162 changelog := gitlabDict(fmt.Sprintf("/repository/changelog?version=%s", desc))
163 outMap["description"] = changelog["notes"]
164 bs, err := json.MarshalIndent(outMap, "", " ")
165 if err != nil {
166 return err
167 }
168 out = string(bs)
169 } else if *javascript {
170 out = fmt.Sprintf(tmplJS, desc, ts, u)
171 } else if *homebrew {
172 bs := bytes.Buffer{}
173 versionNoV := strings.TrimPrefix(desc, "v")
174 darwinAmd64File := fmt.Sprintf("streamplace-%s-darwin-amd64.tar.gz", desc)
175 darwinArm64File := fmt.Sprintf("streamplace-%s-darwin-arm64.tar.gz", desc)
176 linuxAmd64File := fmt.Sprintf("streamplace-%s-linux-amd64.tar.gz", desc)
177 linuxArm64File := fmt.Sprintf("streamplace-%s-linux-arm64.tar.gz", desc)
178
179 err = homebrewTmpl.Execute(&bs, Homebrew{
180 Version: versionNoV,
181 DarwinArm64: getHash(darwinArm64File),
182 DarwinAmd64: getHash(darwinAmd64File),
183 LinuxArm64: getHash(linuxArm64File),
184 LinuxAmd64: getHash(linuxAmd64File),
185 })
186 if err != nil {
187 return err
188 }
189 out = bs.String()
190 } else {
191 out = fmt.Sprintf(tmpl, desc, ts, u)
192 }
193
194 if *output != "" {
195 if err := os.WriteFile(*output, []byte(out), 0644); err != nil {
196 return err
197 }
198 } else {
199 fmt.Print(out)
200 }
201 return nil
202}
203
204func getHash(fileName string) string {
205 filePath := fmt.Sprintf("bin/%s", fileName)
206 f, err := os.Open(filePath)
207 if err != nil {
208 panic(err)
209 }
210 defer f.Close()
211
212 h := sha256.New()
213 buf := make([]byte, 1024*1024) // 1MB buffer
214
215 for {
216 n, err := f.Read(buf)
217 if n > 0 {
218 if _, err := h.Write(buf[:n]); err != nil {
219 panic(err)
220 }
221 }
222 if err != nil {
223 if err == io.EOF {
224 break
225 }
226 panic(err)
227 }
228 }
229
230 return fmt.Sprintf("%x", h.Sum(nil))
231}
232
233func branch() string {
234 CICommitTag := os.Getenv("CI_COMMIT_TAG")
235 CICommitBranch := os.Getenv("CI_COMMIT_BRANCH")
236 if CICommitTag != "" {
237 return "latest"
238 } else if CICommitBranch != "" {
239 return strings.ReplaceAll(CICommitBranch, "/", "-")
240 } else {
241 panic("CI_COMMIT_TAG and CI_COMMIT_BRANCH undefined, can't get branch")
242 }
243}
244
245// Git struct wrapps Repository class from go-git to add a tag map used to perform queries when describing.
246type Git struct {
247 TagsMap map[plumbing.Hash]*plumbing.Reference
248 *git.Repository
249}
250
251// PlainOpen opens a git repository from the given path. It detects if the
252// repository is bare or a normal one. If the path doesn't contain a valid
253// repository ErrRepositoryNotExists is returned
254func PlainOpen(path string) (*Git, error) {
255 r, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true})
256 return &Git{
257 make(map[plumbing.Hash]*plumbing.Reference),
258 r,
259 }, err
260}
261
262func (g *Git) getTagMap() error {
263 tags, err := g.Tags()
264 if err != nil {
265 return err
266 }
267
268 err = tags.ForEach(func(t *plumbing.Reference) error {
269 h, err := g.ResolveRevision(plumbing.Revision(t.Name()))
270 if err != nil {
271 return err
272 }
273 g.TagsMap[*h] = t
274 return nil
275 })
276
277 return err
278}
279
280// Describe the reference as 'git describe --tags' will do
281func (g *Git) Describe(reference *plumbing.Reference) (string, error) {
282 if os.Getenv("STREAMPLACE_VERSION_OVERRIDE") != "" {
283 return os.Getenv("STREAMPLACE_VERSION_OVERRIDE"), nil
284 }
285
286 // Fetch the reference log
287 cIter, err := g.Log(&git.LogOptions{
288 // From: reference.Hash(),
289 Order: git.LogOrderCommitterTime,
290 })
291 if err != nil {
292 return "", err
293 }
294
295 // Build the tag map
296 err = g.getTagMap()
297 if err != nil {
298 return "", err
299 }
300
301 // Search the tag
302 var tag *plumbing.Reference
303 var count int
304 err = cIter.ForEach(func(c *object.Commit) error {
305 t, ok := g.TagsMap[c.Hash]
306 if ok {
307 tag = t
308 return storer.ErrStop
309 }
310 count++
311 return nil
312 })
313 if err != nil {
314 return "", err
315 }
316 head, err := g.Head()
317 if err != nil {
318 return "", err
319 }
320 if count == 0 && os.Getenv("CI_COMMIT_TAG") != "" {
321 return fmt.Sprint(tag.Name().Short()), nil
322 } else {
323 return fmt.Sprintf("%s-%s",
324 tag.Name().Short(),
325 head.Hash().String()[0:8],
326 ), nil
327 }
328}
329
330type Homebrew struct {
331 Version string
332 DarwinArm64 string
333 DarwinAmd64 string
334 LinuxArm64 string
335 LinuxAmd64 string
336}
337
338var homebrewTmpl = template.Must(template.New("homebrew").Parse(`
339class Streamplace < Formula
340 desc "Live video for the AT Protocol. Solving video for everybody forever."
341 homepage "https://stream.place"
342 license "GPL-3.0-or-later"
343 version "{{.Version}}"
344
345 on_macos do
346 if Hardware::CPU.arm?
347 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-darwin-arm64.tar.gz"
348 sha256 "{{.DarwinArm64}}"
349 end
350
351 if Hardware::CPU.intel?
352 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-darwin-amd64.tar.gz"
353 sha256 "{{.DarwinAmd64}}"
354 end
355 end
356
357 on_linux do
358 if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
359 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-linux-arm64.tar.gz"
360 sha256 "{{.LinuxArm64}}"
361 end
362
363 if Hardware::CPU.intel?
364 url "https://git-cloudflare.stream.place/api/v4/projects/1/packages/generic/latest/v{{.Version}}/streamplace-v{{.Version}}-linux-amd64.tar.gz"
365 sha256 "{{.LinuxAmd64}}"
366 end
367 end
368
369 def install
370 bin.install "streamplace" => "streamplace"
371 end
372end
373`))