blob: b16ad9d56f276b46a5e3c01c1047695ba64adeee [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Leopold Schabel18b4d652020-12-14 18:27:07 +01002// SPDX-License-Identifier: Apache-2.0
Leopold Schabel18b4d652020-12-14 18:27:07 +01003
4package watchers
5
6import (
7 "encoding/xml"
8 "fmt"
Leopold Schabel18b4d652020-12-14 18:27:07 +01009 "os"
10 "path"
11)
12
13const (
14 fileWatchersPath = ".ijwb/.idea/watcherTasks.xml"
15 templatePath = "intellij/localconfig/data/watcherTasks.xml"
16)
17
18type config struct {
19 XMLName xml.Name `xml:"project"`
20 Version string `xml:"version,attr"`
21 Component component `xml:"component"`
22}
23
24type component struct {
25 Name string `xml:"name,attr"`
26 TaskOptions []taskOption `xml:"TaskOptions"`
27}
28
29type taskOption struct {
30 IsEnabled string `xml:"isEnabled,attr"`
31 Option []struct {
32 Name string `xml:"name,attr"`
33 Value string `xml:"value,attr,omitempty"`
34 Data string `xml:",innerxml"`
35 } `xml:"option"`
36 Envs struct {
37 Env []struct {
38 Name string `xml:"name,attr"`
39 Value string `xml:"value,attr"`
40 } `xml:"env"`
41 } `xml:"envs"`
42}
43
44func buildConfig(options []taskOption) *config {
45 return &config{
46 XMLName: xml.Name{Local: "project"},
47 Version: "4",
48 Component: component{
49 Name: "ProjectTasksOptions",
50 TaskOptions: options,
51 },
52 }
53}
54
55func readConfig(filename string) (cfg *config, err error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +010056 b, err := os.ReadFile(filename)
Leopold Schabel18b4d652020-12-14 18:27:07 +010057 if err != nil {
58 return nil, fmt.Errorf("failed reading file: %w", err)
59 }
60
61 err = xml.Unmarshal(b, &cfg)
62 if err != nil {
63 return nil, fmt.Errorf("failed deserializing XML: %w", err)
64 }
65
66 return
67}
68
69func (cfg *config) atomicWriteFile(filename string) error {
70 b, err := xml.MarshalIndent(cfg, "", " ")
71 if err != nil {
72 return fmt.Errorf("failed to serialize: %w", err)
73 }
74
Serge Bazanski216fe7b2021-05-21 18:36:16 +020075 // Atomic write is needed, IntelliJ has inotify watches on its config and reloads
76 // (but not applies) instantly.
Leopold Schabel18b4d652020-12-14 18:27:07 +010077 tmpPath := filename + ".tmp"
78 defer os.Remove(tmpPath)
Lorenz Brun764a2de2021-11-22 16:26:36 +010079 if err := os.WriteFile(tmpPath, []byte(xml.Header+string(b)), 0664); err != nil {
Leopold Schabel18b4d652020-12-14 18:27:07 +010080 return fmt.Errorf("failed to write: %w", err)
81 }
82 if err := os.Rename(tmpPath, filename); err != nil {
83 return fmt.Errorf("failed to rename: %w", err)
84 }
85
86 return nil
87}
88
Serge Bazanski216fe7b2021-05-21 18:36:16 +020089// RewriteConfig adds our watchers to projectDir's watchers config, overwriting
90// existing entries with the same name.
Leopold Schabel18b4d652020-12-14 18:27:07 +010091func RewriteConfig(projectDir string) error {
92 template, err := readConfig(path.Join(projectDir, templatePath))
93 if err != nil {
94 return fmt.Errorf("failed reading template config: %w", err)
95 }
96
97 if template.Version != "4" {
98 return fmt.Errorf("unknown template config version: %s", template.Version)
99 }
100
101 // Read existing tasks, if any.
102 tasks := make(map[string]taskOption)
103 cfg, err := readConfig(path.Join(projectDir, fileWatchersPath))
104
105 switch {
106 case err == nil:
107 // existing config, read tasks
108 if cfg.Version != "4" {
109 return fmt.Errorf("unknown watchers config version: %s", cfg.Version)
110 }
111 for _, v := range cfg.Component.TaskOptions {
112 for _, o := range v.Option {
113 if o.Name == "name" {
114 tasks[o.Value] = v
115 }
116 }
117 }
118 case os.IsNotExist(err):
119 // no existing config - continue with empty tasks
120 default:
121 // error is non-nil and not an ENOENT
122 return fmt.Errorf("failed reading existing config: %w", err)
123 }
124
125 // Overwrite "our" entries, identified by name.
126 for _, v := range template.Component.TaskOptions {
127 for _, o := range v.Option {
128 if o.Name == "name" {
129 tasks[o.Value] = v
130 }
131 }
132 }
133
134 // Build new configuration
135 options := make([]taskOption, 0, len(tasks))
136 for _, t := range tasks {
137 options = append(options, t)
138 }
139
140 out := buildConfig(options)
141
142 err = out.atomicWriteFile(path.Join(projectDir, fileWatchersPath))
143 if err != nil {
144 return fmt.Errorf("failed writing to output file: %w", err)
145 }
146
147 return nil
148}