blob: dd1810c583552102203f3b2dd5b40b3cc713bf91 [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
Serge Bazanski216fe7b2021-05-21 18:36:16 +020089 // Atomic write is needed, IntelliJ has inotify watches on its config and reloads
90 // (but not applies) instantly.
Leopold Schabel18b4d652020-12-14 18:27:07 +010091 tmpPath := filename + ".tmp"
92 defer os.Remove(tmpPath)
93 if err := ioutil.WriteFile(tmpPath, []byte(xml.Header+string(b)), 0664); err != nil {
94 return fmt.Errorf("failed to write: %w", err)
95 }
96 if err := os.Rename(tmpPath, filename); err != nil {
97 return fmt.Errorf("failed to rename: %w", err)
98 }
99
100 return nil
101}
102
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200103// RewriteConfig adds our watchers to projectDir's watchers config, overwriting
104// existing entries with the same name.
Leopold Schabel18b4d652020-12-14 18:27:07 +0100105func RewriteConfig(projectDir string) error {
106 template, err := readConfig(path.Join(projectDir, templatePath))
107 if err != nil {
108 return fmt.Errorf("failed reading template config: %w", err)
109 }
110
111 if template.Version != "4" {
112 return fmt.Errorf("unknown template config version: %s", template.Version)
113 }
114
115 // Read existing tasks, if any.
116 tasks := make(map[string]taskOption)
117 cfg, err := readConfig(path.Join(projectDir, fileWatchersPath))
118
119 switch {
120 case err == nil:
121 // existing config, read tasks
122 if cfg.Version != "4" {
123 return fmt.Errorf("unknown watchers config version: %s", cfg.Version)
124 }
125 for _, v := range cfg.Component.TaskOptions {
126 for _, o := range v.Option {
127 if o.Name == "name" {
128 tasks[o.Value] = v
129 }
130 }
131 }
132 case os.IsNotExist(err):
133 // no existing config - continue with empty tasks
134 default:
135 // error is non-nil and not an ENOENT
136 return fmt.Errorf("failed reading existing config: %w", err)
137 }
138
139 // Overwrite "our" entries, identified by name.
140 for _, v := range template.Component.TaskOptions {
141 for _, o := range v.Option {
142 if o.Name == "name" {
143 tasks[o.Value] = v
144 }
145 }
146 }
147
148 // Build new configuration
149 options := make([]taskOption, 0, len(tasks))
150 for _, t := range tasks {
151 options = append(options, t)
152 }
153
154 out := buildConfig(options)
155
156 err = out.atomicWriteFile(path.Join(projectDir, fileWatchersPath))
157 if err != nil {
158 return fmt.Errorf("failed writing to output file: %w", err)
159 }
160
161 return nil
162}