Skip to content

Commit 7203f1e

Browse files
author
obsidian
committed
vault backup: 2025-03-13 15:39:09 M source/_posts/flutter 无胶水代码构建打包 ffi plugin.md
Affected files: source/_posts/flutter 无胶水代码构建打包 ffi plugin.md
1 parent 74f5e6f commit 7203f1e

File tree

1 file changed

+239
-74
lines changed

1 file changed

+239
-74
lines changed

source/_posts/flutter 无胶水代码构建打包 ffi plugin.md

Lines changed: 239 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,94 +37,272 @@ quickjs 主要有两个版本:
3737
1) git clone quickjs 并 checkout 到 v0.7.0 tag
3838
2) 编写 xmake 脚本,见[xmake quickjs-ng 交叉编译实践](https://blog.dang8080.cn/2024/11/17/09/)
3939
3) 编译生成跨平台动态库
40+
4) 将各个平台的动态库打包为 tar.gz
4041

4142
2、新建 flutter ffi plugin project
4243

4344
#android
44-
`build.gradle` `dependencies` 中添加 `implementation('cn.humphreyd.flujs-android:quickjs:0.6.1.2')`
45-
#iOS
45+
~~`build.gradle` `dependencies` 中添加 `implementation('cn.humphreyd.flujs-android:quickjs:0.6.1.2')`~~
46+
> 上面是需要手动将 qjs 动态库打包为aar,然后发布到 mvn 仓库.详情见[安卓发布纯so库到maven](https://blog.dang8080.cn/2024/12/01/00/)
47+
48+
> 相比较之下还是比较麻烦,所以直接使用 gradle 脚本下载 so 到 jniLibs 并打包
49+
50+
```groovy
51+
def targetAbis = android.defaultConfig.ndk.abiFilters
52+
def vers = Prop("flujs.qjs.qjs_version", "8.0.0")
53+
def baseUrl = Prop("flujs.qjs.baseUrl", "https://gitee.com/flujs_project/flujs_libs_quickjs/releases/download/${vers}")
54+
def forceDownload = Prop("flujs.qjs.force_download", "false").toBoolean()
55+
56+
targetAbis.each { abi ->
57+
println("[🌹] ${abi}")
58+
59+
def jniLibsDir = file("$projectDir/src/main/jniLibs/${abi}")
60+
def soFile = file("${jniLibsDir}/libqjs.so")
61+
62+
if (forceDownload || !soFile.exists()) {
63+
def downloadTaskName = "downloadQjs${abi.capitalize()}"
64+
tasks.register(downloadTaskName, DownloadTask) {
65+
group 'QuickJS'
66+
arch = abi
67+
sourceUrl = "${baseUrl}/qjs_android_${abi}.tar.gz"
68+
destDir = file("${project.buildDir}/tmp/qjs/${abi}")
69+
}
70+
71+
def tarFile = file("${tasks.getByName(downloadTaskName).destDir}/qjs_android_${abi}.tar.gz")
72+
73+
def extractTaskName = "extractQjs${abi.capitalize()}"
74+
tasks.register(extractTaskName, Copy) {
75+
group 'QuickJS'
76+
dependsOn tasks.getByName(downloadTaskName)
77+
from({ tarTree(
78+
tarFile.path.endsWith('.gz') ?
79+
resources.gzip(tarFile) :
80+
resources.bzip2(tarFile)
81+
)})
82+
into "$projectDir/src/main/jniLibs/${abi}"
83+
includeEmptyDirs = false
84+
}
85+
preBuild.dependsOn tasks.getByName(extractTaskName)
86+
} else {
87+
println("[🌹] libqjs.so already exists for ${abi}, skipping download and extraction. use flutter clean, and rerun if you insist on download and extraction. or Set flujs.qjs.force_download=true to force download.")
88+
}
89+
}
90+
91+
class DownloadTask extends DefaultTask {
92+
@Input String arch
93+
@Input String sourceUrl
94+
@Input boolean overwrite = false
95+
@OutputDirectory File destDir
96+
97+
@TaskAction
98+
void download() {
99+
destDir.mkdirs()
100+
def outputFile = new File(destDir, "qjs_android_${arch}.tar.gz")
101+
println("[🌹] DownloadTask ✈️ arch=${arch},sourceUrl=${sourceUrl},overwrite=${overwrite},outputFile=${outputFile}")
102+
if (!outputFile.exists() || overwrite) {
103+
URL url = new URL(sourceUrl)
104+
HttpURLConnection connection = (HttpURLConnection) url.openConnection()
105+
connection.setInstanceFollowRedirects(true) // 启用自动重定向
106+
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") // 添加 User-Agent
107+
connection.connect()
108+
109+
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
110+
connection.getInputStream().withStream { input ->
111+
outputFile.withOutputStream { it << input }
112+
}
113+
} else {
114+
throw new IOException("Failed to download file: HTTP ${connection.getResponseCode()}, ${connection.getURL().toString()}")
115+
}
116+
}
117+
}
118+
}
119+
120+
def Prop(String name, String defaultValue = null) {
121+
if (project.hasProperty(name)) {
122+
return project.findProperty(name)
123+
} else if (System.getenv(name) != null) {
124+
return System.getenv(name)
125+
} else {
126+
return defaultValue
127+
}
128+
}
129+
```
130+
46131
```ruby
47-
archs = ENV['ARCHS'] || 'arm64'
48-
spec.subspec 'Framework' do |framework|
49-
framework.vendored_frameworks = "Frameworks/qjs.xcframework/ios-#{archs}/qjs.framework"
132+
require "open-uri"
133+
require "pathname"
134+
require "fileutils"
135+
require "digest"
136+
137+
QJS_VERSION = ENV["QJS_VERSION"] || "0.8.0".freeze
138+
QJS_BASEURL = ENV["QJS_BASEURL"] || "https://gitee.com/flujs_project/flujs_libs_quickjs/releases/download/#{QJS_VERSION}"
139+
140+
class FlujsUtil
141+
attr_reader :framework_path
142+
143+
def initialize(platform)
144+
@framework_path = ""
145+
@force_download = ENV["FORCE_DOWNLOAD"] == "true"
146+
147+
framework_lib = "Frameworks/libqjs.dylib"
148+
cache_lib = ".cache/libqjs.dylib"
149+
150+
if File.exist?(framework_lib) && !@force_download
151+
puts "[darwin] libqjs.dylib already exists in Frameworks/, skipping download and extraction.use flutter clean, and rerun if you insist on download and extraction. or set FORCE_DOWNLOAD=true"
152+
@framework_path = "Frameworks/"
153+
return
154+
elsif File.exist?(cache_lib)
155+
puts "[darwin] libqjs.dylib found in .cache/, execute extraction."
156+
@framework_path = "Frameworks/"
157+
FileUtils.mkdir_p(@framework_path)
158+
FileUtils.cp(cache_lib, @framework_path)
159+
return
160+
end
161+
162+
FileUtils.mkdir_p('.cache')
163+
puts "[darwin] #{platform} libqjs.dylib not found, downloading and extracting..."
164+
_download(platform)
165+
end
166+
167+
def _download(platform)
168+
# make destdir
169+
dest_dir = ".cache/"
170+
tar_name = ENV["QJS_TAR_NAME"] || "qjs_#{platform}.tar.gz"
171+
tar_path = "#{dest_dir}/#{tar_name}"
172+
# fetch from remote
173+
_fetch("#{QJS_BASEURL}/#{tar_name}", tar_path)
174+
# unzip
175+
_unzip(tar_path, dest_dir)
176+
@framework_path = "Frameworks/"
177+
FileUtils.mkdir_p("#{@framework_path}")
178+
FileUtils.cp("#{dest_dir}/libqjs.dylib", "#{@framework_path}")
179+
end
180+
181+
def _unzip(file, destination)
182+
system("tar -xzf #{file} -C #{destination}")
183+
end
184+
185+
def _fetch(link, dest)
186+
begin
187+
URI.open(link) do |file|
188+
File.open(dest, "wb") do |output|
189+
output.write(file.read)
190+
end
191+
end
192+
puts "[_fetch] succeed #{dest}"
193+
rescue StandardError => e
194+
puts "[_fetch] failed: #{e.message}, #{link}"
195+
end
196+
end
197+
50198
end
51199
```
200+
#iOS
201+
```ruby
202+
flujsutil = FlujsUtil.new('iphoneos')
203+
spec.vendored_libraries = "Frameworks/libqjs.dylib"
204+
205+
spec.preserve_paths = "Frameworks/libqjs.dylib"
206+
```
52207
#macOS
53208
```ruby
54-
archs = ENV['ARCHS'] || 'arm64'
55-
spec.subspec 'Framework' do |framework|
56-
# framework.vendored_frameworks = 'Frameworks/**/*.framework'
57-
framework.vendored_frameworks = "Frameworks/qjs.xcframework/macos-#{archs}/qjs.framework"
58-
end
209+
flujsutil = FlujsUtil.new('macosx')
210+
spec.vendored_libraries = "Frameworks/libqjs.dylib"
211+
212+
spec.preserve_paths = "Frameworks/libqjs.dylib"
59213
```
60214
#linux/windows
61215
linux 和 mac 都使用 cmake 编译,所以这里共用一套逻辑.
62216

63217
`script/artifact.cmake`
64218
```cmake
65-
set(BASEURL_GITEE "https://gitee.com/flujs/flujs_libs_quickjs/releases/download/")
66-
set(BASEURL_GITHUB "https://gitee.com/flujs/flujs_libs_quickjs/releases/download/")
67-
set(QJS_VERSION "v0.6.1.1")
68-
set(QJS_LINUX_ARCHIVE_arm64_MD5 "fde244492cd7c525cad267a03955188b")
69-
set(QJS_LINUX_ARCHIVE_x86_64_MD5 "b83e80e0007be64e81593f6fd5647c30")
70-
set(QJS_MINGW_ARCHIVE_x86_64_MD5 "74150ccfa52c43ca462dd6cd8b891bfd")
71-
72-
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
73-
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64")
74-
set(QJS_LIB_NAME "qjs_linux_x86_64.tar.gz")
75-
set(QJS_ARCHIVE_MD5 "${QJS_LINUX_ARCHIVE_x86_64_MD5}")
76-
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
77-
set(QJS_LIB_NAME "qjs_linux_arm64.tar.gz")
78-
set(QJS_ARCHIVE_MD5 "${QJS_LINUX_ARCHIVE_arm64_MD5}")
79-
else()
80-
message(FATAL_ERROR "Unknown CPU!")
81-
endif()
82-
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
83-
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "X86" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "AMD64" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "IA64")
84-
set(QJS_LIB_NAME "qjs_mingw_x86_64.zip")
85-
set(QJS_ARCHIVE_MD5 "${QJS_MINGW_ARCHIVE_x86_64_MD5}")
86-
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
87-
set(CPU "arm64")
88-
message(FATAL_ERROR "Unknown CPU!")
89-
else()
90-
message(FATAL_ERROR "Unknown CPU!")
219+
# Set BASEURL from environment variable or use default
220+
if(DEFINED ENV{BASEURL})
221+
set(BASEURL "$ENV{BASEURL}")
222+
else()
223+
set(BASEURL "https://gitee.com/flujs_project/flujs_libs_quickjs/releases/download")
224+
endif()
225+
226+
# Set QJS_VERSION from environment variable or use default
227+
if(DEFINED ENV{QJS_VERSION})
228+
set(QJS_VERSION "$ENV{QJS_VERSION}")
229+
else()
230+
set(QJS_VERSION "0.8.0")
231+
endif()
232+
233+
if(DEFINED ENV{QJS_TAR_NAME})
234+
set(QJS_TAR_NAME "$ENV{QJS_TAR_NAME}")
235+
elseif(NOT DEFINED QJS_TAR_NAME)
236+
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
237+
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64")
238+
set(QJS_TAR_NAME "qjs_linux_x86_64.tar.gz")
239+
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
240+
set(QJS_TAR_NAME "qjs_linux_arm64.tar.gz")
241+
else()
242+
message(FATAL_ERROR "Unknown CPU!")
243+
endif()
244+
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
245+
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "X86" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "AMD64" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "IA64")
246+
set(QJS_TAR_NAME "qjs_mingw_x86_64.zip")
247+
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
248+
set(CPU "arm64")
249+
message(FATAL_ERROR "Unknown CPU!")
250+
else()
251+
message(FATAL_ERROR "Unknown CPU!")
252+
endif()
91253
endif()
92254
endif()
93255
256+
if(DEFINED ENV{FORCE_DOWNLOAD})
257+
set(FORCE_DOWNLOAD "$ENV{FORCE_DOWNLOAD}")
258+
else()
259+
set(FORCE_DOWNLOAD FALSE)
260+
endif()
261+
94262
# download quickjs and set QJS_LIBRARY
95263
function(prepareQJS)
96-
set(QJS_LIB_CACHE "${CMAKE_BINARY_DIR}/${QJS_LIB_NAME}")
97-
set(QJS_LIB_PATH "${CMAKE_BINARY_DIR}/release")
98-
set(URL "${BASEURL_GITEE}/${QJS_VERSION}/${QJS_LIB_NAME}")
99-
downloadVerify("${URL}" ${QJS_LIB_CACHE} "${QJS_ARCHIVE_MD5}")
264+
set(QJS_TAR_PATH "${CMAKE_BINARY_DIR}/${QJS_TAR_NAME}")
265+
266+
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
267+
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/libqjs.so") # cannot use PARENT_SCOPE, it will not check the existence of QJS_LIBRARY
268+
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
269+
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/release/lib/libqjs.dll")
270+
endif()
271+
272+
message(STATUS "[prepareQJS] QJS_LIBRARY = ${QJS_LIBRARY} ${FORCE_DOWNLOAD}")
273+
if(EXISTS "${QJS_LIBRARY}" AND NOT FORCE_DOWNLOAD)
274+
message(STATUS "[prepareQJS] ${QJS_LIBRARY} already exists, skipping download and extraction. use flutter clean, and rerun if you insist on download and extraction. or set FORCE_DOWNLOAD=true")
275+
set(QJS_LIBRARY "${QJS_LIBRARY}" PARENT_SCOPE)
276+
return()
277+
endif()
278+
279+
message(STATUS "[prepareQJS] ${QJS_LIBRARY_PATH} not found, downloading and extracting...")
280+
set(URL "${BASEURL}/${QJS_VERSION}/${QJS_TAR_NAME}")
281+
downloadVerify("${URL}" ${QJS_TAR_PATH})
100282
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
101283
add_custom_target(
102284
"QJS_EXTRACT"
103285
ALL
104-
COMMAND "${CMAKE_COMMAND}" -E tar xz "${QJS_LIB_CACHE}"
286+
COMMAND "${CMAKE_COMMAND}" -E tar xz "${QJS_TAR_PATH}"
105287
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
106288
COMMENT "Extracting QJS library"
107289
)
108-
set(QJS_LIBRARY "${QJS_LIB_PATH}/lib/libqjs.so" PARENT_SCOPE)
290+
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/libqjs.so" PARENT_SCOPE)
109291
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
110-
execute_process(COMMAND powershell -Command "Expand-Archive -Path '${QJS_LIB_CACHE}' -DestinationPath '${CMAKE_BINARY_DIR}' -Force" RESULT_VARIABLE extractResult)
292+
execute_process(COMMAND powershell -Command "Expand-Archive -Path '${QJS_TAR_PATH}' -DestinationPath '${CMAKE_BINARY_DIR}' -Force" RESULT_VARIABLE extractResult)
111293
if(NOT extractResult EQUAL 0)
112-
message(FATAL_ERROR "Failed to extract ZIP file: ${QJS_LIB_CACHE}")
294+
message(FATAL_ERROR "Failed to extract ZIP file: ${QJS_TAR_PATH}")
113295
endif()
114-
set(QJS_LIBRARY "${QJS_LIB_PATH}/lib/libqjs.dll" PARENT_SCOPE)
296+
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/release/lib/libqjs.dll" PARENT_SCOPE)
115297
endif()
116298
endfunction(prepareQJS)
117299
118-
# download url resource to locationForArchive and check md5
119-
function(downloadVerify url locationForArchive md5 )
300+
# download url resource to locationForArchive
301+
function(downloadVerify url locationForArchive )
120302
message(STATUS "[downloadVerify] info = ${url}")
121-
if (EXISTS "${locationForArchive}")
122-
file(MD5 "${locationForArchive}" ARCHIVE_MD5)
123-
message(STATUS "[downloadVerify] md5 = ${ARCHIVE_MD5}")
124-
if(NOT md5 STREQUAL ARCHIVE_MD5)
125-
file(REMOVE "${locationForArchive}")
126-
message(WARN "[downloadVerify] MD5 mismatch. ${locationForArchive} deleted!")
127-
endif()
303+
if (EXISTS "${locationForArchive}" AND NOT FORCE_DOWNLOAD)
304+
message(STATUS "[downloadVerify] ${locationForArchive} exists!.")
305+
return()
128306
endif()
129307
130308
if(NOT EXISTS "${locationForArchive}")
@@ -138,14 +316,6 @@ function(downloadVerify url locationForArchive md5 )
138316
if(NOT status_code EQUAL 0)
139317
message(FATAL_ERROR "Error downloading ${download_log}")
140318
endif()
141-
if(NOT md5)
142-
file(MD5 "${locationForArchive}" ARCHIVE_MD5)
143-
if(md5 STREQUAL ARCHIVE_MD5)
144-
message(STATUS "[downloadVerify] ${locationForArchive} Verification successful.")
145-
else()
146-
message(FATAL_ERROR_ERROR "[downloadVerify] ${locationForArchive} Integrity check failed, please try to rebuild project again.")
147-
endif()
148-
endif(NOT md5)
149319
endif()
150320
message(STATUS "[downloadVerify] succeed! ${locationForArchive}")
151321
endfunction(downloadVerify url md5 locationForArchive)
@@ -167,22 +337,16 @@ function(checkDirectoryEmpty dir result)
167337
endfunction(checkDirectoryEmpty)
168338
```
169339

170-
这里首先需要将 linux/windows 的库上传到可访问的地址,然后通过 cmake 下载.
171-
核心点是设置 `set(QJS_LIBRARY "${QJS_LIB_PATH}/lib/libqjs.so" PARENT_SCOPE)` 动态库的地址
172-
173-
然后在 `qjs/src/CMakeLists.txt` 中添加 `QJS` target(也可以在上面的 cmake 脚本中设置)
340+
> 因为 linux/windows 公用一套逻辑,所以将 QJS 设置为 IMPORTED 对象,并拆分为共用的 CMakeLists.txt, 然后在各自 module 中 add_subdirectory 即可
174341
```cmake
175342
cmake_minimum_required(VERSION 3.10)
176343
project(${PROJECT_NAME} VERSION 0.0.1 LANGUAGES C)
177344
178-
set(PLUGIN_NAME "flujs") # for current scope
179-
set(PLUGIN_NAME ${PLUGIN_NAME} PARENT_SCOPE) # for parent scope
180-
181345
set(QJS "quickjs") # for current scope
182346
set(QJS ${QJS} PARENT_SCOPE) # for parent scope
183347
184348
# add qjs prebuilt library
185-
include("${CMAKE_CURRENT_SOURCE_DIR}/../../script/artifact.cmake")
349+
include("${CMAKE_CURRENT_SOURCE_DIR}/../script/artifact.cmake")
186350
prepareQJS()
187351
add_library(${QJS} SHARED IMPORTED GLOBAL)
188352
set_target_properties(${QJS} PROPERTIES IMPORTED_LOCATION "${QJS_LIBRARY}")
@@ -195,9 +359,10 @@ add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../qjs/src" "${CMAKE_CURRENT_BINAR
195359
# List of absolute paths to libraries that should be bundled with the plugin.
196360
# This list could contain prebuilt libraries, or libraries created by an
197361
# external build triggered from this build file.
198-
# !!! must be called in this scope,otherwise qjs lib will not be bundled into package.
199-
set("${PLUGIN_NAME}_bundled_libraries" $<TARGET_FILE:${QJS}> PARENT_SCOPE)
362+
set("flujs_qjs_bundled_libraries" $<TARGET_FILE:${QJS}> PARENT_SCOPE)
200363
```
364+
> 注意: linux/windows cmake 中必须要设置 `set("flujs_qjs_bundled_libraries" $<TARGET_FILE:${QJS}> PARENT_SCOPE)` `$<TARGET_FILE:${QJS}>` 就是 `libqjs.dll/libqjs.so` 的位置,这样打包时 cmake 才会通过 install 命令将动态库复制到产物目录
201365
202-
`set("${PLUGIN_NAME}_bundled_libraries" $<TARGET_FILE:${QJS}> PARENT_SCOPE)` 必须设置,否则动态库无法打包到产物里.
203-
366+
> [flujs](https://gitee.com/flujs_project/flujs)
367+
> [flujs_libs_quickjs](https://gitee.com/flujs_project/flujs_libs_quickjs)
368+
> [flujs_android_quickjs](https://gitee.com/flujs_project/flujs_android_quickjs)

0 commit comments

Comments
 (0)