intellij: add localconfig helper
Adds a little helper tool which merges a watcherTasks template with the local
project config. This restores the functionality lost in D658.
Also cured me of any remaining nostalgic feelings towards XML.
Test Plan:
Deleted all watchers, ran the script, re-opened project,
watchers are back and functional. Local watchers with the same name got
overwritten. Additional watchers were untouched.
X-Origin-Diff: phab/D661
GitOrigin-RevId: 83f7c1506476378145781c816d776fd451aed40c
diff --git a/intellij/localconfig/BUILD.bazel b/intellij/localconfig/BUILD.bazel
new file mode 100644
index 0000000..9125284
--- /dev/null
+++ b/intellij/localconfig/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["localconfig.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/intellij/localconfig",
+ visibility = ["//visibility:private"],
+ deps = ["//intellij/localconfig/watchers:go_default_library"],
+)
+
+go_binary(
+ name = "localconfig",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/intellij/localconfig/data/watcherTasks.xml b/intellij/localconfig/data/watcherTasks.xml
new file mode 100644
index 0000000..4d6e6a8
--- /dev/null
+++ b/intellij/localconfig/data/watcherTasks.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectTasksOptions">
+ <TaskOptions isEnabled="true">
+ <option name="arguments" value="build //core/api/... //core/internal/..." />
+ <option name="checkSyntaxErrors" value="true" />
+ <option name="description" />
+ <option name="exitCodeBehavior" value="ERROR" />
+ <option name="fileExtension" value="proto" />
+ <option name="immediateSync" value="false" />
+ <option name="name" value="Regenerate protobuf files" />
+ <option name="output" value="" />
+ <option name="outputFilters">
+ <array />
+ </option>
+ <option name="outputFromStdout" value="false" />
+ <option name="program" value="$WorkspaceRoot$/scripts/bin/bazel" />
+ <option name="runOnExternalChanges" value="true" />
+ <option name="scopeName" value="All Places" />
+ <option name="trackOnlyRoot" value="false" />
+ <option name="workingDir" value="" />
+ <envs />
+ </TaskOptions>
+ <TaskOptions isEnabled="true">
+ <option name="arguments" value="-local git.monogon.dev -w $FilePath$" />
+ <option name="checkSyntaxErrors" value="true" />
+ <option name="description" />
+ <option name="exitCodeBehavior" value="ERROR" />
+ <option name="fileExtension" value="go" />
+ <option name="immediateSync" value="false" />
+ <option name="name" value="goimports" />
+ <option name="output" value="$FilePath$" />
+ <option name="outputFilters">
+ <array />
+ </option>
+ <option name="outputFromStdout" value="false" />
+ <option name="program" value="$USER_HOME$/.local/bin/goimports" />
+ <option name="runOnExternalChanges" value="false" />
+ <option name="scopeName" value="Project Files" />
+ <option name="trackOnlyRoot" value="true" />
+ <option name="workingDir" value="$ProjectFileDir$" />
+ <envs>
+ <env name="GOROOT" value="$GOROOT$" />
+ <env name="GOPATH" value="$GOPATH$" />
+ <env name="PATH" value="$GoBinDirs$" />
+ </envs>
+ </TaskOptions>
+ </component>
+</project>
diff --git a/intellij/localconfig/localconfig.go b/intellij/localconfig/localconfig.go
new file mode 100644
index 0000000..44ef730
--- /dev/null
+++ b/intellij/localconfig/localconfig.go
@@ -0,0 +1,50 @@
+// 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.
+
+// localconfig modifies the project's IntelliJ config to include project-specific settings. This is usually done by
+// checking in the .idea directory, but we do not want to do this: it conflicts with the Bazel plugin's way of
+// conducting its workspace business, lacks backwards compatibility, and is a common source of spurious Git diffs,
+// especially when the IDE/JDK/random plugins are updated and team members run different versions.
+//
+// Instead, we use the officially supported way of shipping IntelliJ Bazel project configs - a .bazelproject file that
+// can be imported using the Bazel project import wizard, with local configs. We then use this tool to mangle the local
+// configs to add additional custom configuration beyond run configurations. This avoids merge conflicts and allows us
+// to intelligently handle schema changes.
+//
+package main
+
+import (
+ "log"
+ "os"
+ "path"
+
+ "git.monogon.dev/source/nexantic.git/intellij/localconfig/watchers"
+)
+
+func main() {
+ if len(os.Args) != 2 {
+ log.Fatal("usage: localconfig <project dir>")
+ }
+
+ projectDir := os.Args[1]
+ if _, err := os.Stat(path.Join(projectDir, ".ijwb")); err != nil {
+ log.Fatalf("invalid project dir: %v", err)
+ }
+
+ if err := watchers.RewriteConfig(projectDir); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/intellij/localconfig/watchers/BUILD.bazel b/intellij/localconfig/watchers/BUILD.bazel
new file mode 100644
index 0000000..bae47c7
--- /dev/null
+++ b/intellij/localconfig/watchers/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["filewatchers.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/intellij/localconfig/watchers",
+ visibility = ["//visibility:public"],
+)
diff --git a/intellij/localconfig/watchers/filewatchers.go b/intellij/localconfig/watchers/filewatchers.go
new file mode 100644
index 0000000..f31a26a
--- /dev/null
+++ b/intellij/localconfig/watchers/filewatchers.go
@@ -0,0 +1,160 @@
+// 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
+}