使用IntelliJ将版本信息添加到jar文件名

时间:2017-01-16 16:27:11

标签: intellij-idea gradle versioning

我已经习惯了Android Studio并开发了Android项目。

在Android Studio中,我把它放在build.gradle文件中:

defaultConfig {
    applicationId "com.domain.myapp"
    minSdkVersion 19
    targetSdkVersion 19
    versionCode 1
    versionName "1.0"
    setProperty("archivesBaseName", "myapp.$versionName.$versionCode")
    signingConfig signingConfigs.config
}
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        applicationVariants.all { variant ->
            variant.outputs.each { output ->
                def newName = output.outputFile.name
                newName = newName.replace("-release", "")
                output.outputFile = new File(output.outputFile.parent, newName)
            }
        }
        signingConfig signingConfigs.config
    }
    debug {
        signingConfig signingConfigs.config
    }
}

当我构建时,我得到myapp.1.0.1.apk,效果非常好。

现在我正在使用IntelliJ开发一个java项目.jar,而不是Android Studio。

我怎样才能完成同样的事情?我找到了稀疏的信息......

1 个答案:

答案 0 :(得分:0)

Android通过添加构建R.java文件的任务来实现此目的。就像Android一样简单使它看起来复制行为需要花费一点力气。您可以创建自己的gradle任务来完成相同的任务。您将需要一个创建和扩展以及任务的gradle插件。该扩展程序将用于跟踪build.gradle

中添加的值

插件应该创建扩展和任务

    // create DSL model
    target.extensions.create('buildConfig', BuildConfigModel)
    // create task to generate file
    def buildConfigTask = target.tasks.create('generateBuildConfig', BuildConfigTask)
    target.tasks.findByName('clean').configure {
        delete new File("$project.projectDir/src/main", "generated-sources")
    }
    // this task must always run... it's never `upToDate`
    buildConfigTask.outputs.upToDateWhen { false }
    // java compiler depends on this task... allows us to reference generated code from
    // human written code
    target.tasks.getByName('compileJava').dependsOn buildConfigTask

以下是如何使用您的任务添加生成的源文件

    project.configure(project, { Project configuredProject ->
        // compilerJava task {@link JavaCompile}
        def compileJava = configuredProject.compileJava
        // add the created java file to source path so it gets compiled
        compileJava.source(project.buildConfig.createBuildConfig(project, compileJava))
    })

然后我们的扩展看起来像这样

package com.jbirdvegas.q41680813;

import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.tasks.compile.JavaCompile;

import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Handles creating the BuildConfig.java from a module project
 * <p>
 * Warning... Keep this in java, gradle doesn't have the streams api we are using
 */
public class BuildConfigModel {
    /**
     * Final java file output path pattern for {@link String#format(String, Object...) String#format} formatting of
     * the file's path.  Directory structure will be created if needed
     */
    private static final String OUTPUT_PATH_FORMAT = "%s/src/main/generated-sources/%s/%s/BuildConfig.java";

    /**
     * List of DSL supplied {@link BuildValue buildConfig#add}
     */
    private List<BuildValue> mBuildValues = new ArrayList<>();

    /**
     * Required... do not remove
     */
    public BuildConfigModel() {
    }

    /**
     * Create a new field to the project's generated `BuildConfig.java`'s inner class for each type
     *
     * @param clazz Type of value to be written (will be grouped by clazz)
     * @param name  field name to be created
     * @param value value to be assigned to field's name
     */
    @SuppressWarnings({"unused", "WeakerAccess"})
    public void add(Class clazz, String name, Object value) {
        mBuildValues.add(new BuildValue(clazz, name, value));
    }

    /**
     * Create `BuildConfig.java` and add it to the {@link JavaCompile#source(Object...)}  compileJava#source(Object...)}
     *
     * @param project     module to generate BuildConfig for
     * @param javaCompile project's `compileJava` task
     * @return generated `BuildConfig.java` file
     */
    public File createBuildConfig(Project project, JavaCompile javaCompile) {
        File buildConfigFile = getBuildConfigFile(project);
        createJavaClass(project, buildConfigFile);
        javaCompile.source(buildConfigFile);
        return buildConfigFile;
    }

    /**
     * Destination file for given project's `BuildConfig.java`
     *
     * @param project module to generate BuildConfig for
     * @return File representing the destination of the created `BuildConfig.java`
     */
    @SuppressWarnings("WeakerAccess")
    public File getBuildConfigFile(Project project) {
        return project.file(String.format(OUTPUT_PATH_FORMAT,
                project.getProjectDir().getAbsolutePath(),
                project.getGroup().toString().replaceAll("\\.", "/"),
                project.getName()));
    }

    /**
     * Create `BuildConfig.java` with a few default values and any values supplied
     * to the `buildConfig`'s {@link #add(Class, String, Object) add} method.
     * <p>
     * Default BuildConfig fields will be generated by {@link #getDefaultFields}
     * <p>
     * Fields added via {@link #add(Class, String, Object) add} method will be grouped into inner classes
     * named <pre>{@code Class#getSimpleName().toLowerCase() + "s"}</pre>
     *
     * @param project         module to generate BuildConfig for
     * @param buildConfigFile File representing the destination of the BuildConfig.java output
     */
    @SuppressWarnings("WeakerAccess")
    public void createJavaClass(Project project, File buildConfigFile) {
        //noinspection unchecked
        Collections.sort(mBuildValues);
        // group our configs by their types into a map of groups
        Map<Class, List<BuildValue>> groupedConfigs = mBuildValues.stream()
                // put the values in groups based on the simple name of the class in lowercase
                .collect(Collectors.groupingBy(BuildValue::getValueType));

        // create the fully qualified class that will contain our build settings
        TypeSpec.Builder buildConfigJavaBuilder = TypeSpec.classBuilder("BuildConfig")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                // note for javadoc
                .addJavadoc("$S\n", "DO NOT MODIFY; this class is written automatically by the compiler")
                // replace public constructor with private
                .addMethod(createPrivateConstructor());

        // add any fields that will be in all BuildConfig classes
        buildConfigJavaBuilder.addFields(getDefaultFields(project));

        groupedConfigs.forEach((aClass, buildValues) -> {
            // create the inner class
            String safeInnerClassName = aClass.getSimpleName().toLowerCase() + 's';
            TypeSpec.Builder innerClass = TypeSpec.classBuilder(safeInnerClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    // make a constructor that's private since all the members of this class are public static final
                    .addMethod(createPrivateConstructor());
            // for each inner class type create a field
            // each object type gets it's own inner class
            //noinspection SimplifyStreamApiCallChains
            buildValues.stream().forEachOrdered(buildValue -> {
                // create the requested field in the class
                FieldSpec fieldSpec = FieldSpec.builder(buildValue.clazz, buildValue.name)
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                        .initializer(CodeBlock.of(getStringFormatter(buildValue.clazz), buildValue.value))
                        .build();
                // add the field to the inner class
                innerClass.addField(fieldSpec);
            });
            // add the inner class to the fully qualified class
            buildConfigJavaBuilder.addType(innerClass.build());
        });

        // determine the package name from project.group + '.' + project.name
        String packageName = project.getGroup() + "." + project.getName();
        // create a java file writer
        JavaFile.Builder builder = JavaFile.builder(packageName, buildConfigJavaBuilder.build());
        // don't import java.lang.* it's redundant
        builder.skipJavaLangImports(true);
        // use four spaces for indent instead of default two spaces
        builder.indent("    ");
        // create the file in memory
        JavaFile javaFile = builder.build();

        // ensure file structure
        if (!buildConfigFile.getParentFile().exists() && !buildConfigFile.getParentFile().mkdirs()) {
            throw new GradleException("Failed to create directory structure for " + buildConfigFile.getAbsolutePath());
        }

        // write BuildConfig.java to location
        try (FileWriter writer = new FileWriter(buildConfigFile)) {
            javaFile.writeTo(writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Strings require being treated specially in order to be encapsulated in quotes correctly
     * All other classes are treated as literals... We may want to handle more {@link java.lang.reflect.Type Type}
     *
     * @param clazz Class formatter is needed for
     * @return "$S" if the class is a {@link String} else a literal formatter is returned "$L"
     */
    private String getStringFormatter(Class clazz) {
        switch (clazz.getSimpleName().toLowerCase()) {
            case "string":
                // causes the formatter used to wrap the value in quotes correctly
            case "date":
                // date objects are serialized to a string
                return "$S";
            case "long":
                return "$LL";
            case "double":
                return "$LD";
            case "float":
                return "$LF";
            default:
                // for the reset use literal
                return "$L";
        }
    }

    /**
     * get project added build values
     *
     * @return List of build values added by project's `buildConfig` closure
     */
    @SuppressWarnings("unused")
    public List<BuildValue> collectBuildValues() {
        return mBuildValues;
    }

    /**
     * Make a private constructor for the class. Default is public but our classes only contain
     * <pre>{@code public static final {@link Object}}</pre> so public constructors are redundant
     *
     * @return private constructor method
     */
    private MethodSpec createPrivateConstructor() {
        return MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build();
    }

    /**
     * Create default field references
     *
     * @param project module to generate BuildConfig for
     * @return List of fields to write to generated BuildConfig
     */
    private List<FieldSpec> getDefaultFields(Project project) {
        List<FieldSpec> fields = new ArrayList<>();

        // set current version
        fields.add(FieldSpec.builder(String.class, "version")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .initializer(CodeBlock.of(getStringFormatter(String.class), project.getVersion()))
                .build());
        return fields;
    }

    class BuildValue implements Comparable {
        /**
         * Type of field's value
         */
    /* package */ Class clazz;

        /**
         * Field name
         */
    /* package */ String name;

        /**
         * Field's value Value must be able to be serialized as a string
         */
    /* package */ Object value;

        /* package */ BuildValue(Class clazz, String name, Object value) {
            this.clazz = clazz;
            this.name = name;
            this.value = value;
        }

        /* package */ Class getValueType() {
            return clazz;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof BuildValue)) return false;

            BuildValue that = (BuildValue) o;

            if (clazz != null ? !clazz.equals(that.clazz) : that.clazz != null) return false;
            if (name != null ? !name.equals(that.name) : that.name != null) return false;
            return value != null ? value.equals(that.value) : that.value == null;
        }

        @Override
        public int hashCode() {
            int result = clazz != null ? clazz.hashCode() : 0;
            result = 31 * result + (name != null ? name.hashCode() : 0);
            result = 31 * result + (value != null ? value.hashCode() : 0);
            return result;
        }

        @Override
        public int compareTo(Object o) {
            return (name != null && o != null) ? name.compareTo(o.toString()) : -1;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("BuildValue{");
            sb.append("class=").append(clazz.getCanonicalName());
            sb.append(", name='").append(name).append('\'');
            sb.append(", value=").append(value);
            sb.append('}');
            return sb.toString();
        }
    }
}

默认情况下,插件会创建BuildConfig.java,其默认字段为version,但您也可以添加自己的值

buildConfig {
    add Boolean, 'myKey', false
}

然后在运行时,您可以使用BuildConfig.value获取引用的值,对于上面的示例,您还可以将字段BuildConfig.myKey作为Boolean类型提供。

编辑:我的示例使用groovy作为插件类和任务类,但扩展名BuildConfigModel是用java编写的。我的所有来源都位于src/main/groovy