blob: 4a56161de5aa2f131a8602aeee3df2ebe677dab1 [file] [log] [blame]
Leopold Schabel18b4d652020-12-14 18:27:07 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package watchers
18
19import (
20 "encoding/xml"
21 "fmt"
Leopold Schabel18b4d652020-12-14 18:27:07 +010022 "os"
23 "path"
24)
25
26const (
27 fileWatchersPath = ".ijwb/.idea/watcherTasks.xml"
28 templatePath = "intellij/localconfig/data/watcherTasks.xml"
29)
30
31type config struct {
32 XMLName xml.Name `xml:"project"`
33 Version string `xml:"version,attr"`
34 Component component `xml:"component"`
35}
36
37type component struct {
38 Name string `xml:"name,attr"`
39 TaskOptions []taskOption `xml:"TaskOptions"`
40}
41
42type taskOption struct {
43 IsEnabled string `xml:"isEnabled,attr"`
44 Option []struct {
45 Name string `xml:"name,attr"`
46 Value string `xml:"value,attr,omitempty"`
47 Data string `xml:",innerxml"`
48 } `xml:"option"`
49 Envs struct {
50 Env []struct {
51 Name string `xml:"name,attr"`
52 Value string `xml:"value,attr"`
53 } `xml:"env"`
54 } `xml:"envs"`
55}
56
57func buildConfig(options []taskOption) *config {
58 return &config{
59 XMLName: xml.Name{Local: "project"},
60 Version: "4",
61 Component: component{
62 Name: "ProjectTasksOptions",
63 TaskOptions: options,
64 },
65 }
66}
67
68func readConfig(filename string) (cfg *config, err error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +010069 b, err := os.ReadFile(filename)
Leopold Schabel18b4d652020-12-14 18:27:07 +010070 if err != nil {
71 return nil, fmt.Errorf("failed reading file: %w", err)
72 }
73
74 err = xml.Unmarshal(b, &cfg)
75 if err != nil {
76 return nil, fmt.Errorf("failed deserializing XML: %w", err)
77 }
78
79 return
80}
81
82func (cfg *config) atomicWriteFile(filename string) error {
83 b, err := xml.MarshalIndent(cfg, "", " ")
84 if err != nil {
85 return fmt.Errorf("failed to serialize: %w", err)
86 }
87
Serge Bazanski216fe7b2021-05-21 18:36:16 +020088 // Atomic write is needed, IntelliJ has inotify watches on its config and reloads
89 // (but not applies) instantly.
Leopold Schabel18b4d652020-12-14 18:27:07 +010090 tmpPath := filename + ".tmp"
91 defer os.Remove(tmpPath)
Lorenz Brun764a2de2021-11-22 16:26:36 +010092 if err := os.WriteFile(tmpPath, []byte(xml.Header+string(b)), 0664); err != nil {
Leopold Schabel18b4d652020-12-14 18:27:07 +010093 return fmt.Errorf("failed to write: %w", err)
94 }
95 if err := os.Rename(tmpPath, filename); err != nil {
96 return fmt.Errorf("failed to rename: %w", err)
97 }
98
99 return nil
100}
101
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200102// RewriteConfig adds our watchers to projectDir's watchers config, overwriting
103// existing entries with the same name.
Leopold Schabel18b4d652020-12-14 18:27:07 +0100104func RewriteConfig(projectDir string) error {
105 template, err := readConfig(path.Join(projectDir, templatePath))
106 if err != nil {
107 return fmt.Errorf("failed reading template config: %w", err)
108 }
109
110 if template.Version != "4" {
111 return fmt.Errorf("unknown template config version: %s", template.Version)
112 }
113
114 // Read existing tasks, if any.
115 tasks := make(map[string]taskOption)
116 cfg, err := readConfig(path.Join(projectDir, fileWatchersPath))
117
118 switch {
119 case err == nil:
120 // existing config, read tasks
121 if cfg.Version != "4" {
122 return fmt.Errorf("unknown watchers config version: %s", cfg.Version)
123 }
124 for _, v := range cfg.Component.TaskOptions {
125 for _, o := range v.Option {
126 if o.Name == "name" {
127 tasks[o.Value] = v
128 }
129 }
130 }
131 case os.IsNotExist(err):
132 // no existing config - continue with empty tasks
133 default:
134 // error is non-nil and not an ENOENT
135 return fmt.Errorf("failed reading existing config: %w", err)
136 }
137
138 // Overwrite "our" entries, identified by name.
139 for _, v := range template.Component.TaskOptions {
140 for _, o := range v.Option {
141 if o.Name == "name" {
142 tasks[o.Value] = v
143 }
144 }
145 }
146
147 // Build new configuration
148 options := make([]taskOption, 0, len(tasks))
149 for _, t := range tasks {
150 options = append(options, t)
151 }
152
153 out := buildConfig(options)
154
155 err = out.atomicWriteFile(path.Join(projectDir, fileWatchersPath))
156 if err != nil {
157 return fmt.Errorf("failed writing to output file: %w", err)
158 }
159
160 return nil
161}