// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.analysis;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.OptionsUtils.PathFragmentConverter;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
import java.io.Serializable;
import javax.annotation.Nullable;

/** A configuration fragment that tells where the shell is. */
public class ShellConfiguration extends Fragment {
  private static final ImmutableMap<OS, PathFragment> OS_SPECIFIC_SHELL =
      ImmutableMap.<OS, PathFragment>builder()
          .put(OS.WINDOWS, PathFragment.create("c:/tools/msys64/usr/bin/bash.exe"))
          .put(OS.FREEBSD, PathFragment.create("/usr/local/bin/bash"))
          .put(OS.OPENBSD, PathFragment.create("/usr/local/bin/bash"))
          .build();

  private final PathFragment shellExecutable;
  private final boolean useShBinaryStubScript;

  private ShellConfiguration(PathFragment shellExecutable, boolean useShBinaryStubScript) {
    this.shellExecutable = shellExecutable;
    this.useShBinaryStubScript = useShBinaryStubScript;
  }

  public PathFragment getShellExecutable() {
    return shellExecutable;
  }

  public boolean useShBinaryStubScript() {
    return useShBinaryStubScript;
  }

  /** An option that tells Bazel where the shell is. */
  public static class Options extends FragmentOptions {
    @Option(
        name = "shell_executable",
        converter = PathFragmentConverter.class,
        defaultValue = "null",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
        help =
            "Absolute path to the shell executable for Bazel to use. If this is unset, but the "
                + "BAZEL_SH environment variable is set on the first Bazel invocation (that starts "
                + "up a Bazel server), Bazel uses that. If neither is set, Bazel uses a hard-coded "
                + "default path depending on the operating system it runs on (Windows: "
                + "c:/tools/msys64/usr/bin/bash.exe, FreeBSD: /usr/local/bin/bash, all others: "
                + "/bin/bash). Note that using a shell that is not compatible with bash may lead "
                + "to build failures or runtime failures of the generated binaries."
    )
    public PathFragment shellExecutable;

    @Option(
        name = "experimental_use_sh_binary_stub_script",
        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        metadataTags = {OptionMetadataTag.EXPERIMENTAL},
        defaultValue = "false",
        help = "If enabled, use a stub script for sh_binary targets.")
    public boolean useShBinaryStubScript;

    @Override
    public Options getHost() {
      Options host = (Options) getDefault();
      host.shellExecutable = shellExecutable;
      host.useShBinaryStubScript = useShBinaryStubScript;
      return host;
    }
  }

  /** the part of {@link ShellConfiguration} that determines where the shell is. */
  public interface ShellExecutableProvider {
    PathFragment getShellExecutable(BuildOptions options);
  }

  /** A shell executable whose path is hard-coded. */
  public static ShellExecutableProvider hardcodedShellExecutable(String shell) {
    return (ShellExecutableProvider & Serializable) (options) -> PathFragment.create(shell);
  }

  /** The loader for {@link ShellConfiguration}. */
  public static class Loader implements ConfigurationFragmentFactory {
    private final ShellExecutableProvider shellExecutableProvider;
    private final ImmutableSet<Class<? extends FragmentOptions>> requiredOptions;

    public Loader(ShellExecutableProvider shellExecutableProvider,
        Class<? extends FragmentOptions>... requiredOptions) {
      this.shellExecutableProvider = shellExecutableProvider;
      this.requiredOptions = ImmutableSet.copyOf(requiredOptions);
    }

    @Nullable
    @Override
    public Fragment create(BuildOptions buildOptions) {
      Options options = buildOptions.get(Options.class);
      return new ShellConfiguration(
          shellExecutableProvider.getShellExecutable(buildOptions),
          options != null && options.useShBinaryStubScript);
    }

    public static PathFragment determineShellExecutable(
        OS os, Options options, PathFragment defaultShell) {
      if (options.shellExecutable != null) {
        return options.shellExecutable;
      }

      // Honor BAZEL_SH env variable for backwards compatibility.
      String path = System.getenv("BAZEL_SH");
      if (path != null) {
        return PathFragment.create(path);
      }
      // TODO(ulfjack): instead of using the OS Bazel runs on, we need to use the exec platform,
      // which may be different for remote execution. For now, this can be overridden with
      // --shell_executable, so at least there's a workaround.
      PathFragment result = OS_SPECIFIC_SHELL.get(os);
      return result != null ? result : defaultShell;
    }

    @Override
    public Class<? extends Fragment> creates() {
      return ShellConfiguration.class;
    }

    @Override
    public ImmutableSet<Class<? extends FragmentOptions>> requiredOptions() {
      return requiredOptions;
    }
  }
}
