blob: f31a26ab32675004737c1b97998e54b9d7d4ff50 [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"
22 "io/ioutil"
23 "os"
24 "path"
25)
26
27const (
28 fileWatchersPath = ".ijwb/.idea/watcherTasks.xml"
29 templatePath = "intellij/localconfig/data/watcherTasks.xml"
30)
31
32type config struct {
33 XMLName xml.Name `xml:"project"`
34 Version string `xml:"version,attr"`
35 Component component `xml:"component"`
36}
37
38type component struct {
39 Name string `xml:"name,attr"`
40 TaskOptions []taskOption `xml:"TaskOptions"`
41}
42
43type taskOption struct {
44 IsEnabled string `xml:"isEnabled,attr"`
45 Option []struct {
46 Name string `xml:"name,attr"`
47 Value string `xml:"value,attr,omitempty"`
48 Data string `xml:",innerxml"`
49 } `xml:"option"`
50 Envs struct {
51 Env []struct {
52 Name string `xml:"name,attr"`
53 Value string `xml:"value,attr"`
54 } `xml:"env"`
55 } `xml:"envs"`
56}
57
58func buildConfig(options []taskOption) *config {
59 return &config{
60 XMLName: xml.Name{Local: "project"},
61 Version: "4",
62 Component: component{
63 Name: "ProjectTasksOptions",
64 TaskOptions: options,
65 },
66 }
67}
68
69func readConfig(filename string) (cfg *config, err error) {
70 b, err := ioutil.ReadFile(filename)
71 if err != nil {
72 return nil, fmt.Errorf("failed reading file: %w", err)
73 }
74
75 err = xml.Unmarshal(b, &cfg)
76 if err != nil {
77 return nil, fmt.Errorf("failed deserializing XML: %w", err)
78 }
79
80 return
81}
82
83func (cfg *config) atomicWriteFile(filename string) error {
84 b, err := xml.MarshalIndent(cfg, "", " ")
85 if err != nil {
86 return fmt.Errorf("failed to serialize: %w", err)
87 }
88
89 // Atomic write is needed, IntelliJ has inotify watches on its config and reloads (but not applies) instantly.
90 tmpPath := filename + ".tmp"
91 defer os.Remove(tmpPath)
92 if err := ioutil.WriteFile(tmpPath, []byte(xml.Header+string(b)), 0664); err != nil {
93 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
102// RewriteConfig adds our watchers to projectDir's watchers config, overwriting existing entries with the same name.
103func RewriteConfig(projectDir string) error {
104 template, err := readConfig(path.Join(projectDir, templatePath))
105 if err != nil {
106 return fmt.Errorf("failed reading template config: %w", err)
107 }
108
109 if template.Version != "4" {
110 return fmt.Errorf("unknown template config version: %s", template.Version)
111 }
112
113 // Read existing tasks, if any.
114 tasks := make(map[string]taskOption)
115 cfg, err := readConfig(path.Join(projectDir, fileWatchersPath))
116
117 switch {
118 case err == nil:
119 // existing config, read tasks
120 if cfg.Version != "4" {
121 return fmt.Errorf("unknown watchers config version: %s", cfg.Version)
122 }
123 for _, v := range cfg.Component.TaskOptions {
124 for _, o := range v.Option {
125 if o.Name == "name" {
126 tasks[o.Value] = v
127 }
128 }
129 }
130 case os.IsNotExist(err):
131 // no existing config - continue with empty tasks
132 default:
133 // error is non-nil and not an ENOENT
134 return fmt.Errorf("failed reading existing config: %w", err)
135 }
136
137 // Overwrite "our" entries, identified by name.
138 for _, v := range template.Component.TaskOptions {
139 for _, o := range v.Option {
140 if o.Name == "name" {
141 tasks[o.Value] = v
142 }
143 }
144 }
145
146 // Build new configuration
147 options := make([]taskOption, 0, len(tasks))
148 for _, t := range tasks {
149 options = append(options, t)
150 }
151
152 out := buildConfig(options)
153
154 err = out.atomicWriteFile(path.Join(projectDir, fileWatchersPath))
155 if err != nil {
156 return fmt.Errorf("failed writing to output file: %w", err)
157 }
158
159 return nil
160}