All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jreleaser.engine.hooks.HookExecutor Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2020-2024 The JReleaser authors.
 *
 * 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
 *
 *     https://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 org.jreleaser.engine.hooks;

import org.jreleaser.bundle.RB;
import org.jreleaser.model.JReleaserException;
import org.jreleaser.model.api.hooks.ExecutionEvent;
import org.jreleaser.model.internal.JReleaserContext;
import org.jreleaser.model.internal.hooks.CommandHook;
import org.jreleaser.model.internal.hooks.CommandHooks;
import org.jreleaser.model.internal.hooks.Hook;
import org.jreleaser.model.internal.hooks.Hooks;
import org.jreleaser.model.internal.hooks.ScriptHook;
import org.jreleaser.model.internal.hooks.ScriptHooks;
import org.jreleaser.mustache.TemplateContext;
import org.jreleaser.sdk.command.Command;
import org.jreleaser.sdk.command.CommandException;
import org.jreleaser.sdk.command.CommandExecutor;
import org.jreleaser.util.PlatformUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;

import static java.lang.System.lineSeparator;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.WRITE;
import static org.jreleaser.mustache.Templates.resolveTemplate;
import static org.jreleaser.util.StringUtils.isFalse;
import static org.jreleaser.util.StringUtils.isNotBlank;

/**
 * @author Andres Almiray
 * @since 1.2.0
 */
public final class HookExecutor {
    private static final String JRELEASER_OUTPUT = "JRELEASER_OUTPUT:";
    private final JReleaserContext context;

    public HookExecutor(JReleaserContext context) {
        this.context = context;
    }

    public void execute(String step, Runnable runnable) {
        executeHooks(ExecutionEvent.before(step));

        try {
            runnable.run();
        } catch (RuntimeException e) {
            executeHooks(ExecutionEvent.failure(step, e));
            throw e;
        }
        executeHooks(ExecutionEvent.success(step));
    }

    public void executeHooks(ExecutionEvent event) {
        Hooks hooks = context.getModel().getHooks();
        if (!hooks.isEnabled() || evaluateCondition(hooks.getCondition())) {
            return;
        }

        Map rootEnv = resolveEnvironment(hooks.getEnvironment());
        executeScriptHooks(event, rootEnv);
        executeCommandHooks(event, rootEnv);
    }

    private boolean evaluateCondition(String condition) {
        return isNotBlank(condition) && isFalse(context.eval(condition));
    }

    private Map resolveEnvironment(Map src) {
        Map env = new LinkedHashMap<>();
        TemplateContext props = context.props();
        src.forEach((k, v) -> {
            String value = resolveTemplate(v, props);
            if (isNotBlank(value)) env.put(k, value);
        });
        return env;
    }

    private void executeScriptHooks(ExecutionEvent event, Map rootEnv) {
        ScriptHooks scriptHooks = context.getModel().getHooks().getScript();
        if (!scriptHooks.isEnabled() || evaluateCondition(scriptHooks.getCondition())) {
            return;
        }

        final List hooks = new ArrayList<>();

        switch (event.getType()) {
            case BEFORE:
                hooks.addAll((Collection) filter(scriptHooks.getBefore(), event));
                break;
            case SUCCESS:
                hooks.addAll((Collection) filter(scriptHooks.getSuccess(), event));
                break;
            case FAILURE:
                hooks.addAll((Collection) filter(scriptHooks.getFailure(), event));
                break;
        }

        if (!hooks.isEmpty()) {
            context.getLogger().info(RB.$("hooks.script.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
        }

        context.getLogger().setPrefix("hooks");
        context.getLogger().increaseIndent();

        Map localEnv = new LinkedHashMap<>(rootEnv);
        localEnv.putAll(scriptHooks.getEnvironment());
        localEnv = resolveEnvironment(localEnv);

        try {
            for (ScriptHook hook : hooks) {
                Path scriptFile = null;

                try {
                    scriptFile = createScriptFile(context, hook, event);
                } catch (IOException e) {
                    throw new JReleaserException(RB.$("ERROR_script_hook_create_error"), e);
                }

                String resolvedCmd = hook.getShell().expression().replace("{{script}}", scriptFile.toAbsolutePath().toString());
                executeCommandLine(localEnv, hook, resolvedCmd, resolvedCmd, "ERROR_script_hook_unexpected_error");
            }
        } finally {
            context.getLogger().decreaseIndent();
            context.getLogger().restorePrefix();
        }
    }

    private Path createScriptFile(JReleaserContext context, ScriptHook hook, ExecutionEvent event) throws IOException {
        String scriptContents = hook.getResolvedRun(context, event);
        Path scriptFile = Files.createTempFile("jreleaser", hook.getShell().extension());

        if (hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.PWSH ||
            hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.POWERSHELL) {
            scriptContents = "$ErrorActionPreference = 'stop'" + lineSeparator() + scriptContents;
            scriptContents += lineSeparator() + "if ((Test-Path -LiteralPath variable:\\LASTEXITCODE)) { exit $LASTEXITCODE }";
        }

        Files.write(scriptFile, scriptContents.getBytes(UTF_8), WRITE);
        return scriptFile;
    }

    private void executeCommandHooks(ExecutionEvent event, Map rootEnv) {
        CommandHooks commandHooks = context.getModel().getHooks().getCommand();
        if (!commandHooks.isEnabled() || evaluateCondition(commandHooks.getCondition())) {
            return;
        }

        final List hooks = new ArrayList<>();

        switch (event.getType()) {
            case BEFORE:
                hooks.addAll((Collection) filter(commandHooks.getBefore(), event));
                break;
            case SUCCESS:
                hooks.addAll((Collection) filter(commandHooks.getSuccess(), event));
                break;
            case FAILURE:
                hooks.addAll((Collection) filter(commandHooks.getFailure(), event));
                break;
        }

        if (!hooks.isEmpty()) {
            context.getLogger().info(RB.$("hooks.command.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
        }

        context.getLogger().setPrefix("hooks");
        context.getLogger().increaseIndent();

        Map localEnv = new LinkedHashMap<>(rootEnv);
        localEnv.putAll(commandHooks.getEnvironment());
        localEnv = resolveEnvironment(localEnv);

        try {
            for (CommandHook hook : hooks) {
                String resolvedCmd = hook.getResolvedCmd(context, event);
                executeCommandLine(localEnv, hook, hook.getCmd(), resolvedCmd, "ERROR_command_hook_unexpected_error");
            }
        } finally {
            context.getLogger().decreaseIndent();
            context.getLogger().restorePrefix();
        }
    }

    private void executeCommandLine(Map localEnv, Hook hook, String cmd, String resolvedCmd, String errorKey) {
        List commandLine = null;

        Map hookEnv = new LinkedHashMap<>(localEnv);
        hookEnv.putAll(hook.getEnvironment());
        hookEnv = resolveEnvironment(hookEnv);

        try {
            commandLine = parseCommand(resolvedCmd);
        } catch (IllegalStateException e) {
            throw new JReleaserException(RB.$("ERROR_command_hook_parser_error", cmd), e);
        }

        try {
            Command command = new Command(commandLine);
            processOutput(executeCommand(context.getBasedir(), command, hookEnv, hook.isVerbose()));
        } catch (CommandException e) {
            if (!hook.isContinueOnError()) {
                throw new JReleaserException(RB.$(errorKey), e);
            } else {
                if (null != e.getCause()) {
                    context.getLogger().warn(e.getCause().getMessage());
                } else {
                    context.getLogger().warn(e.getMessage());
                }
                context.getLogger().trace(RB.$(errorKey), e);
            }
        }
    }

    private void processOutput(Command.Result result) {
        if (!result.getOut().contains(JRELEASER_OUTPUT)) return;
        for (String line : result.getOut().split(lineSeparator())) {
            if (!line.startsWith(JRELEASER_OUTPUT)) continue;
            line = line.substring(JRELEASER_OUTPUT.length());
            int p = line.indexOf("=");
            String key = line.substring(0, p);
            String value = line.substring(p + 1);
            context.getModel().getEnvironment().getProperties().put(key, value);
        }
    }

    private Collection filter(List hooks, ExecutionEvent event) {
        List tmp = new ArrayList<>();

        for (Hook hook : hooks) {
            if (!hook.isEnabled() || evaluateCondition(hook.getCondition())) {
                continue;
            }

            if (!hook.getFilter().getResolvedIncludes().isEmpty()) {
                if (hook.getFilter().getResolvedIncludes().contains(event.getName()) && filterByPlatform(hook)) {
                    tmp.add(hook);
                }
            } else if (filterByPlatform(hook)) {
                tmp.add(hook);
            }

            if (hook.getFilter().getResolvedExcludes().contains(event.getName())) {
                tmp.remove(hook);
            }
        }

        return tmp;
    }

    private boolean filterByPlatform(Hook hook) {
        if (hook.getPlatforms().isEmpty()) return true;

        boolean success = true;
        for (String platform : hook.getPlatforms()) {
            boolean exclude = false;
            if (platform.startsWith("!")) {
                exclude = true;
                platform = platform.substring(1);
            }

            success &= exclude != PlatformUtils.isCompatible(PlatformUtils.getCurrentFull(), platform);
        }

        return success;
    }

    private Command.Result executeCommand(Path directory, Command command, Map env, boolean verbose) throws CommandException {
        Command.Result result = new CommandExecutor(context.getLogger(), verbose ? CommandExecutor.Output.VERBOSE : CommandExecutor.Output.DEBUG)
            .environment(env)
            .executeCommand(directory, command);
        if (result.getExitValue() != 0) {
            throw new CommandException(RB.$("ERROR_command_execution_exit_value", result.getExitValue()));
        }
        return result;
    }

    // adjusted from org.apache.tools.ant.types.Commandline#translateCommandLine
    public static List parseCommand(String str) {
        final int normal = 0;
        final int inQuote = 1;
        final int inDoubleQuote = 2;
        int state = normal;
        final StringTokenizer tok = new StringTokenizer(str, "\"' ", true);
        final ArrayList result = new ArrayList<>();
        final StringBuilder current = new StringBuilder();
        boolean lastTokenHasBeenQuoted = false;

        while (tok.hasMoreTokens()) {
            String nextTok = tok.nextToken();
            switch (state) {
                case inQuote:
                    if ("'".equals(nextTok)) {
                        lastTokenHasBeenQuoted = true;
                        state = normal;
                    } else {
                        current.append(nextTok);
                    }
                    break;
                case inDoubleQuote:
                    if ("\"".equals(nextTok)) {
                        lastTokenHasBeenQuoted = true;
                        state = normal;
                    } else {
                        current.append(nextTok);
                    }
                    break;
                default:
                    if ("'".equals(nextTok)) {
                        state = inQuote;
                    } else if ("\"".equals(nextTok)) {
                        state = inDoubleQuote;
                    } else if (" ".equals(nextTok)) {
                        if (lastTokenHasBeenQuoted || current.length() > 0) {
                            result.add(current.toString());
                            current.setLength(0);
                        }
                    } else {
                        current.append(nextTok);
                    }
                    lastTokenHasBeenQuoted = false;
                    break;
            }
        }

        if (lastTokenHasBeenQuoted || current.length() > 0) {
            result.add(current.toString());
        }

        if (state == inQuote || state == inDoubleQuote) {
            throw new IllegalStateException(RB.$("ERROR_unbalanced_quotes", str));
        }

        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy