| // Copyright 2020 The Monogon Project Authors. |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package watchers |
| |
| import ( |
| "encoding/xml" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path" |
| ) |
| |
| const ( |
| fileWatchersPath = ".ijwb/.idea/watcherTasks.xml" |
| templatePath = "intellij/localconfig/data/watcherTasks.xml" |
| ) |
| |
| type config struct { |
| XMLName xml.Name `xml:"project"` |
| Version string `xml:"version,attr"` |
| Component component `xml:"component"` |
| } |
| |
| type component struct { |
| Name string `xml:"name,attr"` |
| TaskOptions []taskOption `xml:"TaskOptions"` |
| } |
| |
| type taskOption struct { |
| IsEnabled string `xml:"isEnabled,attr"` |
| Option []struct { |
| Name string `xml:"name,attr"` |
| Value string `xml:"value,attr,omitempty"` |
| Data string `xml:",innerxml"` |
| } `xml:"option"` |
| Envs struct { |
| Env []struct { |
| Name string `xml:"name,attr"` |
| Value string `xml:"value,attr"` |
| } `xml:"env"` |
| } `xml:"envs"` |
| } |
| |
| func buildConfig(options []taskOption) *config { |
| return &config{ |
| XMLName: xml.Name{Local: "project"}, |
| Version: "4", |
| Component: component{ |
| Name: "ProjectTasksOptions", |
| TaskOptions: options, |
| }, |
| } |
| } |
| |
| func readConfig(filename string) (cfg *config, err error) { |
| b, err := ioutil.ReadFile(filename) |
| if err != nil { |
| return nil, fmt.Errorf("failed reading file: %w", err) |
| } |
| |
| err = xml.Unmarshal(b, &cfg) |
| if err != nil { |
| return nil, fmt.Errorf("failed deserializing XML: %w", err) |
| } |
| |
| return |
| } |
| |
| func (cfg *config) atomicWriteFile(filename string) error { |
| b, err := xml.MarshalIndent(cfg, "", " ") |
| if err != nil { |
| return fmt.Errorf("failed to serialize: %w", err) |
| } |
| |
| // Atomic write is needed, IntelliJ has inotify watches on its config and reloads (but not applies) instantly. |
| tmpPath := filename + ".tmp" |
| defer os.Remove(tmpPath) |
| if err := ioutil.WriteFile(tmpPath, []byte(xml.Header+string(b)), 0664); err != nil { |
| return fmt.Errorf("failed to write: %w", err) |
| } |
| if err := os.Rename(tmpPath, filename); err != nil { |
| return fmt.Errorf("failed to rename: %w", err) |
| } |
| |
| return nil |
| } |
| |
| // RewriteConfig adds our watchers to projectDir's watchers config, overwriting existing entries with the same name. |
| func RewriteConfig(projectDir string) error { |
| template, err := readConfig(path.Join(projectDir, templatePath)) |
| if err != nil { |
| return fmt.Errorf("failed reading template config: %w", err) |
| } |
| |
| if template.Version != "4" { |
| return fmt.Errorf("unknown template config version: %s", template.Version) |
| } |
| |
| // Read existing tasks, if any. |
| tasks := make(map[string]taskOption) |
| cfg, err := readConfig(path.Join(projectDir, fileWatchersPath)) |
| |
| switch { |
| case err == nil: |
| // existing config, read tasks |
| if cfg.Version != "4" { |
| return fmt.Errorf("unknown watchers config version: %s", cfg.Version) |
| } |
| for _, v := range cfg.Component.TaskOptions { |
| for _, o := range v.Option { |
| if o.Name == "name" { |
| tasks[o.Value] = v |
| } |
| } |
| } |
| case os.IsNotExist(err): |
| // no existing config - continue with empty tasks |
| default: |
| // error is non-nil and not an ENOENT |
| return fmt.Errorf("failed reading existing config: %w", err) |
| } |
| |
| // Overwrite "our" entries, identified by name. |
| for _, v := range template.Component.TaskOptions { |
| for _, o := range v.Option { |
| if o.Name == "name" { |
| tasks[o.Value] = v |
| } |
| } |
| } |
| |
| // Build new configuration |
| options := make([]taskOption, 0, len(tasks)) |
| for _, t := range tasks { |
| options = append(options, t) |
| } |
| |
| out := buildConfig(options) |
| |
| err = out.atomicWriteFile(path.Join(projectDir, fileWatchersPath)) |
| if err != nil { |
| return fmt.Errorf("failed writing to output file: %w", err) |
| } |
| |
| return nil |
| } |