/* * Copyright 2000-2011 JetBrains s.r.o. * * 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 jetbrains.buildServer.agent.rakerunner; import com.intellij.util.containers.HashMap; import java.io.File; import java.util.*; import jetbrains.buildServer.RunBuildException; import jetbrains.buildServer.agent.rakerunner.utils.*; import jetbrains.buildServer.agent.runner.BuildServiceAdapter; import jetbrains.buildServer.agent.runner.ProgramCommandLine; import jetbrains.buildServer.agent.runner.SimpleProgramCommandLine; import jetbrains.buildServer.rakerunner.RakeRunnerConstants; import jetbrains.buildServer.rakerunner.RakeRunnerUtils; import jetbrains.buildServer.runner.BuildFileRunnerUtil; import jetbrains.buildServer.util.PropertiesUtil; import jetbrains.buildServer.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.ruby.rvm.RVMSupportUtil; import static jetbrains.buildServer.agent.rakerunner.utils.FileUtil.getCanonicalPath; import static jetbrains.buildServer.runner.BuildFileRunnerConstants.BUILD_FILE_PATH_KEY; /** * @author Roman.Chernyatchik */ public class RakeTasksBuildService extends BuildServiceAdapter implements RakeRunnerConstants { private final Set myFilesToDelete = new HashSet(); private final String RSPEC_RUNNER_OPTIONS_REQUIRE_KEY = "--require"; private final String RSPEC_RUNNER_OPTIONS_FORMATTER_PATH = "teamcity/spec/runner/formatter/teamcity/formatter"; private final String RSPEC_RUNNERR_OPTIONS_FORMATTER_KEY = "--format"; private final String RSPEC_RUNNERR_OPTIONS_FORMATTER_CLASS = "Spec::Runner::Formatter::TeamcityFormatter"; private final String CUCUMBER_RUNNER_OPTIONS_EXPAND_KEY = "--expand"; private final String CUCUMBER_RUNNER_OPTIONS_FORMAT_KEY = "--format"; private final String CUCUMBER_RUNNER_OPTIONS_FORMAT_CLASS = "Teamcity::Cucumber::Formatter"; @NotNull @Override public ProgramCommandLine makeProgramCommandLine() throws RunBuildException { List arguments = new ArrayList(); final Map runParams = new HashMap(getRunnerParameters()); final Map buildParams = new HashMap(getBuildParameters().getAllParameters()); // apply options converter SupportedTestFramework.convertOptionsIfNecessary(runParams); // runParams - all server-ui options // buildParams - system properties (system.*), environment vars (env.*) final Map buildEnvVars = getBuildParameters().getEnvironmentVariables(); final Map runnerEnvParams = new HashMap(buildEnvVars); final File buildFile = getBuildFile(runParams); final RakeRunnerUtils.RubyConfigMode interpreterConfigMode = RakeRunnerUtils.getRubyInterpreterConfigMode(runParams); final boolean rubyEnvAlreadyConfigured = Boolean.valueOf(runParams.get(SharedRubyEnvSettings.SHARED_RUBY_PARAMS_ARE_SET)); try { // configure params in terms of "shared params" configureRunnerParams(interpreterConfigMode, rubyEnvAlreadyConfigured, runParams); final String checkoutDirPath = getCanonicalPath(getCheckoutDirectory()); // Sdk final RubySdk sdk = RubySDKUtil.createAndSetupSdk(runParams, buildParams, buildEnvVars); if (!(interpreterConfigMode == RakeRunnerUtils.RubyConfigMode.DEFAULT && rubyEnvAlreadyConfigured)) { // 1. default, but build feature wasn't configured // 2. rvm or interpreter path // Inspect env, warn about any problems // (if defaults were set by smb else we cannot check them) RVMSupportUtil.inspectCurrentEnvironment(runnerEnvParams, sdk, getBuild().getBuildLogger()); if (sdk.isRvmSdk()) { // Patch env for RVM RVMSupportUtil.patchEnvForRVMIfNecessary(sdk, runnerEnvParams); if (sdk.isSystemRvm()) { // Also Patch path for fake RVM RubySDKUtil.patchPathEnvForNonRvmOrSystemRvmSdk(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath); } } else { if (interpreterConfigMode == RakeRunnerUtils.RubyConfigMode.INTERPRETER_PATH) { // non-rvm sdk RubySDKUtil.patchPathEnvForNonRvmOrSystemRvmSdk(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath); } } } // loadpath patch for test runners addTestRunnerPatchFiles(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath); // attached frameworks info if (SupportedTestFramework.isAnyFrameworkActivated(runParams)) { runnerEnvParams.put(RAKERUNNER_USED_FRAMEWORKS_KEY, SupportedTestFramework.getActivatedFrameworksConfig(runParams)); } // track invoke/execute stages if (ConfigurationParamsUtil.isTraceStagesOptionEnabled(runParams)) { runnerEnvParams.put(RAKE_TRACE_INVOKE_EXEC_STAGES_ENABLED_KEY, Boolean.TRUE.toString()); } // Rake runner script final String rakeRunnerPath; final String customRakeRunnerScript = buildParams.get(CUSTOM_RAKERUNNER_SCRIPT); if (!StringUtil.isEmpty(customRakeRunnerScript)) { // use custom runner rakeRunnerPath = customRakeRunnerScript; } else { // default one rakeRunnerPath = RubyProjectSourcesUtil.getRakeRunnerPath(); } arguments.add(rakeRunnerPath); // Rake gem version addGemVersionAttribute(arguments, RAKE_GEM_VERSION_PROPERTY, buildParams); // Rake options // Custom Rakefile if specified if (buildFile != null) { arguments.add(RAKE_CMDLINE_OPTIONS_RAKEFILE); arguments.add(buildFile.getAbsolutePath()); } // Other arguments final String otherArgsString = runParams.get(SERVER_UI_RAKE_ADDITIONAL_CMD_PARAMS_PROPERTY); if (!TextUtil.isEmptyOrWhitespaced(otherArgsString)) { addCmdlineArguments(arguments, otherArgsString); } // Tasks names final String tasks_names = runParams.get(SERVER_UI_RAKE_TASKS_PROPERTY); if (!PropertiesUtil.isEmptyOrNull(tasks_names)) { addCmdlineArguments(arguments, tasks_names); } // rspec attachRSpecFormatterIfNeeded(runParams, runnerEnvParams); // cucumber attachCucumberFormatterIfNeeded(runParams, runnerEnvParams); // Bunlde exec emulation: // (do not do it befor RVM Env patch!!!!!!) BundlerUtil.enableBundleExecEmulationIfNeeded(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath); // Result: return new SimpleProgramCommandLine(runnerEnvParams, getWorkingDirectory().getAbsolutePath(), sdk.getInterpreterPath(), arguments); } catch (MyBuildFailureException e) { throw new RunBuildException(e.getMessage()); } } private void configureRunnerParams(@NotNull final RakeRunnerUtils.RubyConfigMode interpreterConfigMode, final boolean rubyEnvAlreadyConfigured, final Map runParams) throws MyBuildFailureException { switch (interpreterConfigMode) { case DEFAULT: if (rubyEnvAlreadyConfigured) { // check that params were applied if (!Boolean.valueOf(runParams.get(SharedRubyEnvSettings.SHARED_RUBY_PARAMS_ARE_APPLIED))) { throw new MyBuildFailureException( "Ruby interpeter is configured outside Rake build runner but configuration settings weren't applied. No sense to launch rake."); } } else { // if shared params are not defined - use default system interpreter runParams.put(SharedRubyEnvSettings.SHARED_RUBY_PARAMS_ARE_SET, Boolean.TRUE.toString()); runParams.put(SharedRubyEnvSettings.SHARED_RUBY_INTERPRETER_PATH, ""); } break; case INTERPRETER_PATH: final String rubySdkPath = RakeRunnerUtils.getRubySdkPath(runParams); if (StringUtil.isEmpty(rubySdkPath)) { throw new MyBuildFailureException("Ruby interpeter path isn't specified."); } runParams.put(SharedRubyEnvSettings.SHARED_RUBY_PARAMS_ARE_SET, Boolean.TRUE.toString()); runParams.put(SharedRubyEnvSettings.SHARED_RUBY_INTERPRETER_PATH, rubySdkPath); break; case RVM: final String rvmSdkName = RakeRunnerUtils.getRVMSdkName(runParams); if (StringUtil.isEmpty(rvmSdkName)) { throw new MyBuildFailureException("RVM Ruby interpeter name isn't specified."); } runParams.put(SharedRubyEnvSettings.SHARED_RUBY_PARAMS_ARE_SET, Boolean.TRUE.toString()); runParams.put(SharedRubyEnvSettings.SHARED_RUBY_RVM_SDK_NAME, rvmSdkName); runParams.put(SharedRubyEnvSettings.SHARED_RUBY_RVM_GEMSET_NAME, RakeRunnerUtils.getRVMGemsetName(runParams)); break; } } /** * Specify gem version attribute if property is set * @param arguments Cmdline arguments * @param gemVersionProperty Property name * @param buildParams Build params */ private void addGemVersionAttribute(final List arguments, final String gemVersionProperty, final Map buildParams) { final String rakeGemVersion = buildParams.get(gemVersionProperty); if (!StringUtil.isEmpty(rakeGemVersion)) { arguments.add("_" + rakeGemVersion + "_"); } } @Override public void afterProcessFinished() { // Remove tmp files for (File file : myFilesToDelete) { jetbrains.buildServer.util.FileUtil.delete(file); } myFilesToDelete.clear(); } private void attachRSpecFormatterIfNeeded(final Map runParams, final Map env) { //attach RSpec formatter only if spec reporter enabled if (SupportedTestFramework.RSPEC.isActivated(runParams)) { final StringBuilder buff = new StringBuilder(); final String userSpecOpts = runParams.get(SERVER_UI_RAKE_RSPEC_OPTS_PROPERTY); if (!StringUtil.isEmpty(userSpecOpts)) { buff.append(userSpecOpts.trim()).append(' '); } buff.append(RSPEC_RUNNER_OPTIONS_REQUIRE_KEY).append(' '); buff.append(RSPEC_RUNNER_OPTIONS_FORMATTER_PATH).append(' '); buff.append(RSPEC_RUNNERR_OPTIONS_FORMATTER_KEY).append(' '); buff.append(RSPEC_RUNNERR_OPTIONS_FORMATTER_CLASS); final String specOpts = buff.toString(); // Log for user getLogger().message("RSpec Options: " + specOpts); // Set env variable env.put(RAKE_RSPEC_OPTS_PARAM_NAME, specOpts); } } private void attachCucumberFormatterIfNeeded( final Map runParams, final Map env) { //attach Cucumber formatter only if cucumber reporter enabled if (SupportedTestFramework.CUCUMBER.isActivated(runParams)) { final StringBuilder buff = new StringBuilder(); //TODO use additional options when cucumber will support it! //cmd.addParameter(RAKE_CUCUMBER_OPTS_PARAM_NAME + "=" + CUCUMBER_RUNNER_INIT_OPTIONS); final String userCucumberOpts = runParams.get(SERVER_UI_RAKE_CUCUMBER_OPTS_PROPERTY); if (!StringUtil.isEmpty(userCucumberOpts)) { buff.append(userCucumberOpts.trim()).append(' '); } buff.append(CUCUMBER_RUNNER_OPTIONS_EXPAND_KEY).append(' '); buff.append(CUCUMBER_RUNNER_OPTIONS_FORMAT_KEY).append(' '); buff.append(CUCUMBER_RUNNER_OPTIONS_FORMAT_CLASS); final String cucumberOpts = buff.toString(); // Log for user getLogger().message("Cucumber Options: " + cucumberOpts); // Set env variable env.put(RAKE_CUCUMBER_OPTS_PARAM_NAME, cucumberOpts); } } private void addTestRunnerPatchFiles(@NotNull final RubySdk sdk, @NotNull final Map runParams, @NotNull final Map buildParams, @NotNull final Map runnerEnvParams, @NotNull final String checkoutDirPath) throws MyBuildFailureException, RunBuildException { final StringBuilder buff = new StringBuilder(); // common part - for rake taks and tests buff.append(RubyProjectSourcesUtil.getLoadPath_PatchRoot_Common()); // Enable Test::Unit patch for : test::unit, test::spec and shoulda frameworks if (SupportedTestFramework.isTestUnitBasedFrameworksActivated(runParams)) { buff.append(File.pathSeparatorChar); final String testUnitPatchRoot = RubyProjectSourcesUtil.getLoadPath_PatchRoot_TestUnit(); buff.append(testUnitPatchRoot); // for bundler support if (BundlerUtil.isBundleExecEmulationEnabled(runParams)) { runnerEnvParams.put(TEAMCITY_TESTUNIT_REPORTER_PATCH_LOCATION, testUnitPatchRoot); } // due to patching loadpath we replace original autorunner but it is used buy our tests runner runnerEnvParams.put(ORIGINAL_SDK_AUTORUNNER_PATH_KEY, TestUnitUtil.getSDKTestUnitAutoRunnerScriptPath(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath)); runnerEnvParams.put(ORIGINAL_SDK_TESTRUNNERMEDIATOR_PATH_KEY, TestUnitUtil.getSDKTestUnitTestRunnerMediatorScriptPath(sdk, runParams, buildParams, runnerEnvParams, checkoutDirPath)); } // for bdd frameworks if (SupportedTestFramework.CUCUMBER.isActivated(runParams) || SupportedTestFramework.RSPEC.isActivated(runParams)) { buff.append(File.pathSeparatorChar); buff.append(RubyProjectSourcesUtil.getLoadPath_PatchRoot_Bdd()); } // patch loadpath OSUtil.appendToRUBYLIBEnvVariable(buff.toString(), runnerEnvParams); } private void addCmdlineArguments(@NotNull final List argsList, @NotNull final String argsString) { final List stringList = StringUtil.splitHonorQuotes(argsString, ' '); for (String arg : stringList) { argsList.add(TextUtil.stripDoubleQuoteAroundValue(arg)); } } @Nullable private File getBuildFile(Map runParameters) throws RunBuildException { final File buildFile; if (BuildFileRunnerUtil.isCustomBuildFileUsed(runParameters)) { buildFile = BuildFileRunnerUtil.getBuildFile(runParameters); myFilesToDelete.add(buildFile); } else { final String buildFilePath = runParameters.get(BUILD_FILE_PATH_KEY); if (PropertiesUtil.isEmptyOrNull(buildFilePath)) { //use rake defaults buildFile = null; } else { buildFile = BuildFileRunnerUtil.getBuildFile(runParameters); } } return buildFile; } public static class MyBuildFailureException extends Exception { public MyBuildFailureException(@NotNull final String msg) { super(msg); } } }