├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | go.mod.yml 3 | go.mod.json 4 | modules.txt -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dennwc/flatpak-go-mod 2 | 3 | go 1.24.4 4 | 5 | require ( 6 | github.com/goccy/go-yaml v1.18.0 7 | golang.org/x/mod v0.27.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 2 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 3 | golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 4 | golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Denys Smirnov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flatpak Go modules helper 2 | 3 | This CLI tool generates Flatpak sources directives for Go modules. 4 | 5 | It helps avoid exposing network access to Flatpak build step, which is one of the requirements for publishing to Flathub. 6 | 7 | ## Usage 8 | 9 | ```bash 10 | go run github.com/dennwc/flatpak-go-mod@latest ./path/to/your/project 11 | ``` 12 | 13 | It will generate following files: `go.mod.yml` (or `go.mod.json`) and `modules.txt`. 14 | 15 | Contents of `go.mod.yml` (or `go.mod.json`) should be copied into `sources` directives of Flatpak YAML. 16 | These directives will populate `./vendor` during Flatpak build with all dependencies of your project, 17 | downloading them directly from Go modules mirror. 18 | 19 | The `modules.txt` file should be copied into your Flatpak directory, which will be added to `./vendor` directory during Flatpak build as well. 20 | 21 | Then, build your Go project as usual. It should detect `vendor` directory automatically. 22 | You can also pass `-mod=vendor` to force vendoring mode during build. 23 | 24 | ## Output 25 | 26 | ### YAML 27 | 28 | ```yaml 29 | # Workaround for Go modules generated by github.com/dennwc/flatpak-go-mod 30 | - type: file 31 | path: modules.txt 32 | dest: vendor 33 | - type: archive 34 | url: https://proxy.golang.org/golang.org/x/mod/@v/v0.7.0.zip 35 | strip-components: 3 36 | dest: vendor/golang.org/x/mod 37 | sha256: 24abd1db13329873d72034dc27efad09cbc37d39cf28b8ff7bb3c2adc8eedef7 38 | ``` 39 | 40 | ### JSON 41 | 42 | ```json 43 | [ 44 | { 45 | "type": "file", 46 | "path": "modules.txt", 47 | "dest": "vendor" 48 | }, 49 | { 50 | "type": "archive", 51 | "url": "https://proxy.golang.org/golang.org/x/mod/@v/v0.7.0.zip", 52 | "strip-components": 3, 53 | "dest": "vendor/golang.org/x/mod", 54 | "sha256": "24abd1db13329873d72034dc27efad09cbc37d39cf28b8ff7bb3c2adc8eedef7" 55 | } 56 | ] 57 | ``` 58 | 59 | ## Options 60 | 61 | - `line-pref` customizes indentation in YAML/JSON file 62 | - `dest-pref` sets a prefix for `sources.dest` paths in YAML/JSON file 63 | - `json` changes the output format to JSON instead of YAML 64 | - `module-name` optional Flatpak module name (`mymodule` produces `mymodule.go.mod.json` and `mymodule.modules.txt`) 65 | 66 | ## Multi-module Flatpak builds 67 | 68 | When building multiple Go modules in a single Flatpak (e.g. a GObject library written in Go and a main app written in Go that imports said GObject library), use `-module-name` to generate separate vendored dependencies for each: 69 | 70 | ```bash 71 | go run github.com/dennwc/flatpak-go-mod@latest -json -module-name mylibgtk ../mylib-gtk/ # Generate for the library 72 | go run github.com/dennwc/flatpak-go-mod@latest -json . # Generate for the main app 73 | ``` 74 | 75 | If you're working with Go workspaces, make sure to `export GOWORK=off`. 76 | 77 | Then reference them in your Flatpak manifest: 78 | 79 | ```jsonc 80 | // ... 81 | { 82 | "modules": [ 83 | { 84 | "name": "mylibgtk", 85 | "buildsystem": "meson", 86 | "sources": [ 87 | { "type": "dir", "path": "../mylib-gtk/" }, 88 | "mylibgtk.go.mod.json" 89 | ] 90 | }, 91 | { 92 | "name": "mymainapp", 93 | "buildsystem": "meson", 94 | "sources": [{ "type": "dir", "path": "." }, "go.mod.json"] 95 | } 96 | ] 97 | } 98 | // ... 99 | ``` 100 | 101 | ## License 102 | 103 | MIT -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "strings" 16 | 17 | "github.com/goccy/go-yaml" 18 | 19 | "golang.org/x/mod/modfile" 20 | ) 21 | 22 | var ( 23 | fOut = flag.String("out", ".", "output directory") 24 | fDstPref = flag.String("dest-pref", "", "destination prefix for generated sources directives") 25 | fLinePref = flag.String("line-pref", "", "line prefix for YAML/JSON file") 26 | fJSON = flag.Bool("json", false, "use JSON instead of YAML for output") 27 | fModuleName = flag.String("module-name", "", "optional Flatpak module name (mymodule produces mymodule.go.mod.json and mymodule.modules.txt)") 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | if err := run(*fOut, *fDstPref, *fLinePref, *fModuleName, flag.Arg(0)); err != nil { 33 | fmt.Fprintln(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func run(out, pref, lpref, moduleName, path string) error { 39 | if err := goModVendor(path); err != nil { 40 | return err 41 | } 42 | 43 | modulesTxtName := "modules.txt" 44 | goModOutName := "go.mod.yml" 45 | if *fJSON { 46 | goModOutName = "go.mod.json" 47 | } 48 | 49 | if moduleName != "" { 50 | modulesTxtName = moduleName + ".modules.txt" 51 | if *fJSON { 52 | goModOutName = moduleName + ".go.mod.json" 53 | } else { 54 | goModOutName = moduleName + ".go.mod.yml" 55 | } 56 | } 57 | 58 | if err := os.Rename(filepath.Join(path, "vendor", "modules.txt"), filepath.Join(out, modulesTxtName)); err != nil { 59 | return err 60 | } 61 | if err := os.RemoveAll(filepath.Join(path, "vendor")); err != nil { 62 | return err 63 | } 64 | mods, err := goModDownloadJSON(path) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | fpath := filepath.Join(path, "go.mod") 70 | data, err := os.ReadFile(fpath) 71 | if err != nil { 72 | return err 73 | } 74 | gomod, err := modfile.Parse(fpath, data, nil) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | replaced := make(map[string]string) 80 | for _, m := range gomod.Replace { 81 | replaced[m.New.Path] = m.Old.Path 82 | } 83 | 84 | outPath := filepath.Join(out, goModOutName) 85 | 86 | f, err := os.Create(outPath) 87 | if err != nil { 88 | return err 89 | } 90 | defer f.Close() 91 | 92 | modulesTxtEntry := map[string]any{ 93 | "type": "file", 94 | "path": modulesTxtName, 95 | "dest": fmt.Sprintf("%svendor", pref), 96 | } 97 | if moduleName != "" { 98 | modulesTxtEntry["dest-filename"] = "modules.txt" // Go, when running in flatpak-builder, will still expect the file to be at modules.txt 99 | } 100 | files := []map[string]any{modulesTxtEntry} 101 | 102 | for _, m := range mods { 103 | dst, ok := replaced[m.Path] 104 | if !ok { 105 | dst = m.Path 106 | } 107 | h, err := sha256sum(m.Zip) 108 | if err != nil { 109 | return err 110 | } 111 | i := strings.Index(m.Zip, "download") 112 | if i < 0 { 113 | return fmt.Errorf("unsupported zip file path: %q", m.Zip) 114 | } 115 | files = append(files, map[string]any{ 116 | "type": "archive", 117 | "url": fmt.Sprintf("https://proxy.golang.org/%s", m.Zip[i+9:]), 118 | "strip-components": strings.Count(m.Path, "/") + 1, 119 | "dest": fmt.Sprintf("%svendor/%s", pref, dst), 120 | "sha256": h, 121 | }) 122 | } 123 | 124 | if *fJSON { 125 | if _, err := f.WriteString(lpref); err != nil { 126 | return err 127 | } 128 | 129 | enc := json.NewEncoder(f) 130 | enc.SetIndent(lpref, " ") 131 | if err := enc.Encode(files); err != nil { 132 | return err 133 | } 134 | } else { 135 | if _, err := f.WriteString(fmt.Sprintf("%v# Workaround for Go modules generated by github.com/dennwc/flatpak-go-mod\n", lpref)); err != nil { 136 | return err 137 | } 138 | 139 | buf := new(bytes.Buffer) 140 | enc := yaml.NewEncoder(buf) 141 | if err := enc.Encode(files); err != nil { 142 | return err 143 | } 144 | if err := enc.Close(); err != nil { 145 | return err 146 | } 147 | if err := indent(f, buf, lpref); err != nil { 148 | return err 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func indent(w io.Writer, r io.Reader, lpref string) error { 156 | sc := bufio.NewScanner(r) 157 | for sc.Scan() { 158 | if _, err := fmt.Fprintf(w, "%s%s\n", lpref, sc.Text()); err != nil { 159 | return err 160 | } 161 | } 162 | return sc.Err() 163 | } 164 | 165 | func execIn(path string, cmd string, args ...string) error { 166 | c := exec.Command(cmd, args...) 167 | c.Dir = path 168 | c.Stderr = os.Stderr 169 | return c.Run() 170 | } 171 | 172 | func goModVendor(path string) error { 173 | return execIn(path, "go", "mod", "vendor") 174 | } 175 | 176 | type Module struct { 177 | Path string 178 | Version string 179 | Info string 180 | GoMod string 181 | Zip string 182 | Dir string 183 | Sum string 184 | GoModSum string 185 | } 186 | 187 | func goModDownloadJSON(path string) ([]Module, error) { 188 | c := exec.Command("go", "mod", "download", "-json") 189 | c.Dir = path 190 | c.Stderr = os.Stderr 191 | var buf bytes.Buffer 192 | c.Stdout = &buf 193 | if err := c.Run(); err != nil { 194 | return nil, err 195 | } 196 | dec := json.NewDecoder(&buf) 197 | var out []Module 198 | for { 199 | var m Module 200 | err := dec.Decode(&m) 201 | if err == io.EOF { 202 | break 203 | } else if err != nil { 204 | return out, err 205 | } 206 | out = append(out, m) 207 | } 208 | return out, nil 209 | } 210 | 211 | func sha256sum(path string) (string, error) { 212 | f, err := os.Open(path) 213 | if err != nil { 214 | return "", err 215 | } 216 | defer f.Close() 217 | h := sha256.New() 218 | _, err = io.Copy(h, f) 219 | if err != nil { 220 | return "", err 221 | } 222 | return hex.EncodeToString(h.Sum(nil)), nil 223 | } 224 | --------------------------------------------------------------------------------