|  | // 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" | 
|  | "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 := os.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 := os.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 | 
|  | } |