【问题标题】:Adding version info to jar file name using IntelliJ使用 IntelliJ 将版本信息添加到 jar 文件名
【发布时间】:2017-01-16 16:27:11
【问题描述】:

我习惯了 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。

我怎样才能完成同样的事情?我正在寻找稀疏的信息...

【问题讨论】:

    标签: intellij-idea gradle versioning


    【解决方案1】:

    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

    【讨论】:

    • 哇。感谢您的详细回复。我今天无法做到这一点,但我会尽快考虑实施。自己手动设置版本信息似乎更容易,但我的项目列表正在增长,所以从长远来看,设置自动化解决方案是值得的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-27
    • 2016-07-30
    • 2015-04-24
    • 1970-01-01
    相关资源
    最近更新 更多