魅族市场apk不能包含32位so库的解决方案

1,656 阅读3分钟

2023年2月3号,负责发版的同事通知说魅族市场审核不通过,原因是安装包包含了32位库,请处理32位库后再重新提交。 然后我们通过市场手机abi分布分析,还有一部分只支持32位的手机。所以除了魅族市场其他市场还是需要包含32位的apk。

但是我们的包是通过多渠道打包脚本一次打包出来的,其中包含华为、小米、oppo、vivo、魅族、应用宝。

所以现在面临的问题是需要单独对魅族打一次包。我想这种方式不符合我门的风格,是否可以一次打包打所有市场的包,其中魅族市场的包只包含64位so库,其他市场包含32位和64位。

我分析apk包发现魅族和其他市场的唯一区别就包里面少了一个lib/armeabi-v7a/文件夹。是否可以通过脚本在apk打包之后将 lib/armeabi-v7a/ 目录删除,然后再重新签名呢?

其中主要的修改点是增加了下面的代码:

# 如果只保留64位so库,则删除32位so库
if only64abiChannels and str(channel["id"]) in only64abiChannels:
    # 删除lib目录下的armeabi-v7a文件夹
    deleteArmeabiV7a = f"zip {dest_apk_path} -d lib/armeabi-v7a/\*"
    delete_result = subprocess.getoutput(deleteArmeabiV7a)
    print(delete_result)
    if "name not matched: lib/armeabi-v7a/*" not in delete_result:
        print(f"----delete lib/armeabi-v7a/* success in {dest_apk_name}")
        zipalign_out = path.join(base_dir, f"output_{dest_apk_name}")
        # 对删除后的apk资源文件进行首位4个字节的对齐
        delete_result = subprocess.getoutput(f"{apk_zipalign} -p -f -v 4 {dest_apk_path} {zipalign_out}")
        delete_array = delete_result.split('\n')
        if len(delete_array) > 1:
            print(delete_array[-1])
        else:
            delete_result = subprocess.getoutput(f"{apk_zipalign} -c -v 4 {zipalign_out}")
            delete_array = delete_result.split('\n')
            if len(delete_array) > 1:
                print(delete_array[-1])
        # 将对齐后的apk覆盖目标apk
        os.remove(dest_apk_path)
        os.rename(zipalign_out, dest_apk_path)
        print(f"---zipaligned apk: {dest_apk_path}")

下面是多渠道打包脚本的完整代码,当然,这里面还包含了加固脚本的代码没有贴出来,加固的流程在多渠道打包之前,加固脚本是支持部分市场加固的,加固之后也是可以删除32位的so库,已经亲自测试过。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import zipfile
import json
import os, sys
from os import path
import argparse
import shutil
if sys.version_info.major <= 2:
  import urllib2
  import commands as subprocess
  reload(sys)
  sys.setdefaultencoding("utf-8")
else:
  import urllib.request as urllib2
  import subprocess

def getJsonData(str):
    try:
        fjson = open(str, "r")
        jsonobj = json.load(fjson)
        fjson.close
    except IOError:
        jsonobj = json.load(urllib2.urlopen(str))
    return jsonobj

try:
    H = dict(line.strip().split('=') for line in open('local.properties') if not line.startswith('#') and not line.startswith('\n'))
except IOError:
    H = dict(line.strip().split('=') for line in open('../local.properties') if not line.startswith('#') and not line.startswith('\n'))
sdk = H['sdk.dir']

apk_signer_str = subprocess.getoutput("find {sdk}/build-tools/ -name 'apksigner'".format(sdk=sdk))
apk_signer_arr = apk_signer_str.split('\n')
apk_signer = apk_signer_arr[-1]
apk_zipalign = ''
if apk_signer and len(apk_signer) > 0:
    apk_zipalign = path.join(path.dirname(apk_signer), "zipalign")
print(f"---apk_signer: {apk_signer}, apk_zipalign: {apk_zipalign}")

key_password = '123456'
base_dir = "app/build/outputs/apk"
base_apk = "app-debug.apk"
jiagu_base_apk = ''
json_path = ''
key_store = ''
channel_ids = []
default_apk_name = 'app-{code}.apk'

parser = argparse.ArgumentParser()
parser.add_argument('-apk_path', action='store', dest='apk_path', help='the apk path')
parser.add_argument('-json_path', action='store', dest='json_path', help='the channel data config path')
parser.add_argument('-key_password', action='store', dest='key_password', help='the password of keystore')
parser.add_argument('-key_store', action='store', dest='key_store', help='the keystore')
parser.add_argument('-default_apk_name', action='store', dest='default_apk_name', help='the default apk name')
parser.add_argument('-channel_ids', action='store', dest='channel_ids', help='the channel packaged')
parser.add_argument('-ex_channel_ids', action='store', dest='ex_channel_ids', help='the channel to exclude')
parser.add_argument('-jiagu_channel_ids', action='store', dest='jiagu_channel_ids', help='the channel packaged')
parser.add_argument('-jiagu_ex_channel_ids', action='store', dest='jiagu_ex_channel_ids', help='the channel to exclude')
parser.add_argument('-is_enable_jiagu', action='store', dest='is_enable_jiagu', help='the is_enable_jiagu to exclude')
parser.add_argument('-only64abiChannels', action='store', dest="only64abiChannels", help='the 32 bit abi to exclude')

argResult = parser.parse_args()
apk_path = argResult.apk_path
json_path = argResult.json_path
key_password = argResult.key_password
key_store = argResult.key_store
default_apk_name = argResult.default_apk_name
jiagu_default_apk_name = "jiagu_" + default_apk_name

channel_ids = argResult.channel_ids
ex_channel_ids = argResult.ex_channel_ids

is_enable_jiagu = argResult.is_enable_jiagu
jiagu_channel_ids = argResult.jiagu_channel_ids
jiagu_ex_channel_ids = argResult.jiagu_ex_channel_ids

only64abiChannels = argResult.only64abiChannels

if apk_path and len(apk_path) > 0:
    base_dir = path.dirname(apk_path)
    base_apk = path.basename(apk_path)
    print(f"base_dir: {base_dir}, base_apk: {base_apk}")

jiagu_apk_path = ''
if is_enable_jiagu == 'true':
    jiagu_apk_path = subprocess.getoutput(f"find {base_dir} -name '*jiagu_sign.apk'")
    if jiagu_apk_path and len(jiagu_apk_path) > 0:
        jiagu_base_apk = path.basename(jiagu_apk_path)
        print(f"jiagu_base_apk: {jiagu_base_apk}")

if channel_ids and len(channel_ids) > 0:
    channel_ids = channel_ids.split(',')
if ex_channel_ids and len(ex_channel_ids) > 0:
    ex_channel_ids = ex_channel_ids.split(',')

if jiagu_channel_ids and len(jiagu_channel_ids) > 0:
    jiagu_channel_ids = jiagu_channel_ids.split(',')
if jiagu_ex_channel_ids and len(jiagu_ex_channel_ids) > 0:
    jiagu_ex_channel_ids = jiagu_ex_channel_ids.split(',')

print(f"----only64abiChannels: {only64abiChannels}")
if only64abiChannels and len(only64abiChannels) > 0:
    only64abiChannels = only64abiChannels.split(',')

if not key_store:
    key_store_str = subprocess.getoutput("find . -maxdepth 1 -name '*.keystore' ")
    key_store_arr = key_store_str.split('\n')
    print(key_store_arr)
    if key_store_arr[0] == '':
        key_store_str = subprocess.getoutput("find . -maxdepth 1 -name '*.jks' ")
        key_store_arr = key_store_str.split('\n')
    key_store = key_store_arr[0]

data = getJsonData(json_path)

shutil.rmtree(path.join(base_dir, "META-INF"), ignore_errors=True)
os.makedirs(path.join(base_dir, "META-INF"))

def packagingApk(p_channels, p_ex_channel_ids, p_base_apk, p_default_apk_name):
    for channel in data:
        if p_channels and not (str(channel["id"]) in p_channels):
            continue
        if p_ex_channel_ids and (str(channel["id"]) in p_ex_channel_ids):
            continue
        empty_channel_file_name = f'fc-multi-channel-{channel["code"]}-{channel["id"]}'
        empty_channel_file = path.join(base_dir, "META-INF", empty_channel_file_name)
        print(f"---empty_channel_file: {empty_channel_file}, empty_channel_file_name: {empty_channel_file_name}")
        channel_file_writer = open(empty_channel_file, 'w')
        if "extInfo" in channel:
            channel_file_writer.write(json.dumps(channel["extInfo"], ensure_ascii=False))

        channel_file_writer.close()
        dest_apk_name = p_default_apk_name.format(code = channel['code'], id = channel['id'], name = channel['name'])
        dest_apk_path = path.join(base_dir, dest_apk_name)
        src_apk_path = path.join(base_dir, p_base_apk)
        print(f"---src_apk_path: {src_apk_path}, dest_apk_path: {dest_apk_path}")
        shutil.copyfile(src_apk_path, dest_apk_path)

        zipped = zipfile.ZipFile(dest_apk_path, 'a', zipfile.ZIP_DEFLATED)
        zipped.write(empty_channel_file, f"META-INF/{empty_channel_file_name}")
        zipped.close()

        # 如果只保留64位so库,则删除32位so库
        if only64abiChannels and str(channel["id"]) in only64abiChannels:
            # 删除lib目录下的armeabi-v7a文件夹
            deleteArmeabiV7a = f"zip {dest_apk_path} -d lib/armeabi-v7a/\*"
            delete_result = subprocess.getoutput(deleteArmeabiV7a)
            print(delete_result)
            if "name not matched: lib/armeabi-v7a/*" not in delete_result:
                print(f"----delete lib/armeabi-v7a/* success in {dest_apk_name}")
                zipalign_out = path.join(base_dir, f"output_{dest_apk_name}")
                delete_result = subprocess.getoutput(f"{apk_zipalign} -p -f -v 4 {dest_apk_path} {zipalign_out}")
                delete_array = delete_result.split('\n')
                if len(delete_array) > 1:
                    print(delete_array[-1])
                else:
                    delete_result = subprocess.getoutput(f"{apk_zipalign} -c -v 4 {zipalign_out}")
                    delete_array = delete_result.split('\n')
                    if len(delete_array) > 1:
                        print(delete_array[-1])
                os.remove(dest_apk_path)
                os.rename(zipalign_out, dest_apk_path)
                print(f"---zipaligned apk: {dest_apk_path}")

        if apk_signer == '':
            print(f'apksigner {dest_apk_path}: warning no found apksigner command')
        elif key_store == '':
            print(f'apksigner {dest_apk_path}: warning no found keystore')
        else:
            signapk = f"echo '{key_password}' | {apk_signer} sign --ks {key_store} {dest_apk_path}"

            result = subprocess.getoutput(signapk)
            result_array = result.split('\n')
            if len(result_array) > 1:
                print(result)
            else:
                print(f'apksigner {dest_apk_path} success')

if jiagu_base_apk and len(jiagu_base_apk) > 0:
    subprocess.getoutput(f"find {base_dir} -type f -not -name '{base_apk}' -not -name '{jiagu_base_apk}' -name '*.apk' -delete")
    packagingApk(channel_ids, ex_channel_ids, base_apk, default_apk_name)
    packagingApk(jiagu_channel_ids, jiagu_ex_channel_ids, jiagu_base_apk, jiagu_default_apk_name)
else:
    subprocess.getoutput(f"find {base_dir} -type f -not -name '{base_apk}' -name '*.apk' -delete")
    packagingApk(channel_ids, ex_channel_ids, base_apk, default_apk_name)