diff --git a/luci-app-run/Makefile b/luci-app-run/Makefile new file mode 100644 index 00000000..d72b125a --- /dev/null +++ b/luci-app-run/Makefile @@ -0,0 +1,18 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-run +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +LUCI_TITLE:=LuCI support for running makeself .run installers +LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI. +LUCI_DEPENDS:=+luci-base +rpcd +libubox +LUCI_PKGARCH:=all + +PKG_MAINTAINER:=wukongdaily <2666180@gmail.com> + + + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-run/htdocs/luci-static/resources/view/run/index.js b/luci-app-run/htdocs/luci-static/resources/view/run/index.js new file mode 100644 index 00000000..d8a39d2d --- /dev/null +++ b/luci-app-run/htdocs/luci-static/resources/view/run/index.js @@ -0,0 +1,384 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +// ====================== 纯JSON国际化 · 中英双语 ====================== +// 25.12 兼容版:唯一变量名 + 可靠语言检测 +const RUN_LANG = (function () { + try { + // 优先从 cookie 获取(LuCI 最常用方式) + const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/); + if (m) return m[1].substring(0, 2).toLowerCase(); + + // 备选:LuCI 环境变量 + if (window.L && L.env && L.env.lang) + return L.env.lang.substring(0, 2).toLowerCase(); + + return 'zh'; + } catch (e) { + return 'zh'; + } +})(); + +const I18N = { + zh: { + title: "Run安装器", + desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。", + drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。", + choose_file: "选择 .run 文件", + execute: "执行", + clean_up: "清理", + upload_title: "上传 .run 安装包", + log_title: "执行日志", + clean_done: "临时文件与日志已清理。", + only_run: "仅支持 .run 文件。", + prepare_upload: "准备上传:%s (%s)", + upload_failed: "上传失败。", + uploading: "正在上传 %s:%d%%", + upload_err: "上传请求失败。", + upload_invalid: "上传返回格式无效。", + upload_done: "上传完成:%s (%s)", + starting: "正在启动安装器...", + started: "安装器已启动,PID %d。", + running: "安装器正在运行。", + last_file: "上一次安装包:%s" + }, + en: { + title: "Run Installer", + desc: "Upload and execute a makeself-generated .run package on this router.", + drop_tip: "Drop a makeself .run file here, or choose one from your computer.", + choose_file: "Choose .run file", + execute: "Execute", + clean_up: "Clean up", + upload_title: "Upload .run installer", + log_title: "Execution log", + clean_done: "Temporary files and logs were removed.", + only_run: "Only .run files are accepted.", + prepare_upload: "Preparing upload: %s (%s)", + upload_failed: "Upload failed.", + uploading: "Uploading %s: %d%%", + upload_err: "Upload request failed.", + upload_invalid: "Invalid upload response.", + upload_done: "Upload complete: %s (%s)", + starting: "Starting installer...", + started: "Installer started, PID %d.", + running: "Installer is running.", + last_file: "Last installer: %s" + } +}; + +function _(key) { + const str = I18N[RUN_LANG]?.[key] || I18N.zh[key] || key; + return str.format.apply(str, Array.prototype.slice.call(arguments, 1)); +} +// ==================================================================== + +var uploadStart = rpc.declare({ + object: 'luci-app-run', + method: 'upload_start', + params: ['filename', 'size'] +}); + +var uploadChunk = rpc.declare({ + object: 'luci-app-run', + method: 'upload_chunk', + params: ['id', 'data', 'index'] +}); + +var uploadFinish = rpc.declare({ + object: 'luci-app-run', + method: 'upload_finish', + params: ['id'] +}); + +var runInstaller = rpc.declare({ + object: 'luci-app-run', + method: 'run', + params: ['id'] +}); + +var getStatus = rpc.declare({ + object: 'luci-app-run', + method: 'status' +}); + +var readLog = rpc.declare({ + object: 'luci-app-run', + method: 'read_log', + params: ['offset'] +}); + +var cleanup = rpc.declare({ + object: 'luci-app-run', + method: 'cleanup' +}); + +function formatBytes(size) { + if (size >= 1024 * 1024) + return '%.1f MiB'.format(size / 1024 / 1024); + + if (size >= 1024) + return '%.1f KiB'.format(size / 1024); + + return '%d B'.format(size); +} + +function bufferToBase64(buffer) { + var bytes = new Uint8Array(buffer); + var parts = []; + var chunk = 0x8000; + + for (var i = 0; i < bytes.length; i += chunk) + parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunk))); + + return btoa(parts.join('')); +} + +return view.extend({ + // ====================== 底部按钮正确隐藏 ====================== + handleSave: null, + handleReset: null, + handleSaveApply: null, + + logOffset: 0, + currentUploadId: null, + + load: function () { + return getStatus().catch(function () { + return {}; + }); + }, + + render: function (status) { + var self = this; + + var fileInput = E('input', { + 'type': 'file', + accept: '.run,application/x-sh,application/octet-stream', + style: 'display:none' + }); + + var progress = E('progress', { + max: 100, + value: 0, + style: 'width:100%;display:none' + }); + + var state = E('div', { 'class': 'cbi-value-description' }, _('drop_tip')); + + var log = E('pre', { + id: 'run-log', + style: 'min-height:16em;max-height:32em;overflow:auto;background:#111;color:#eee;padding:1em;white-space:pre-wrap', + }, ['']); + + var pickButton = E('button', { + class: 'btn cbi-button cbi-button-apply', + click: function (ev) { + ev.preventDefault(); + fileInput.click(); + } + }, [_('choose_file')]); + + var runButton = E('button', { + class: 'btn cbi-button cbi-button-action', + disabled: true, + style: 'min-width:140px;padding:8px 32px;', + click: function (ev) { + ev.preventDefault(); + self.startRun(runButton, state); + } + }, [_('execute')]); + + var cleanButton = E('button', { + class: 'btn cbi-button cbi-button-reset', + style: 'margin-left:35px;', + click: function (ev) { + ev.preventDefault(); + cleanup().then(function (res) { + if (res && res.error) + throw new Error(res.error); + + self.currentUploadId = null; + self.logOffset = 0; + runButton.disabled = true; + log.textContent = ''; + progress.style.display = 'none'; + state.textContent = _('clean_done'); + }).catch(function (err) { + ui.addNotification(null, E('p', [err.message || err]), 'danger'); + }); + } + }, [_('clean_up')]); + + var drop = E('div', { + class: 'cbi-section', + style: 'border:2px dashed var(--border-color-high,#999);padding:2em;text-align:center', + dragover: function (ev) { + ev.preventDefault(); + drop.style.borderStyle = 'solid'; + }, + dragleave: function () { + drop.style.borderStyle = 'dashed'; + }, + drop: function (ev) { + ev.preventDefault(); + drop.style.borderStyle = 'dashed'; + if (ev.dataTransfer.files && ev.dataTransfer.files.length) + self.uploadFile(ev.dataTransfer.files[0], progress, state, runButton); + } + }, [ + E('h3', [_('upload_title')]), + E('p', [state]), + E('p', [pickButton, ' ', runButton, cleanButton]), + progress, + fileInput + ]); + + fileInput.addEventListener('change', function () { + if (fileInput.files && fileInput.files.length) + self.uploadFile(fileInput.files[0], progress, state, runButton); + }); + + poll.add(function () { + return self.refreshLog(log, state); + }, 1); + + this.applyStatus(status, state); + + return E('div', { class: 'cbi-map' }, [ + E('h2', [_('title')]), + E('div', { class: 'cbi-map-descr', style: 'margin-bottom:15px' }, [_('desc')]), + drop, + E('div', { class: 'cbi-section' }, [ + E('h3', [_('log_title')]), + log + ]) + ]); + }, + + applyStatus: function (status, state) { + if (!status) + return; + + if (status.running) + state.textContent = _('running'); + else if (status.file) + state.textContent = _('last_file', status.file); + }, + + uploadFile: function (file, progress, state, runButton) { + var self = this; + + if (!file.name.match(/\.run$/i)) { + ui.addNotification(null, E('p', [_('only_run')]), 'danger'); + return Promise.reject(); + } + + progress.style.display = ''; + progress.value = 0; + runButton.disabled = true; + state.textContent = _('prepare_upload', file.name, formatBytes(file.size)); + + return uploadStart(file.name, file.size).then(function (res) { + if (res && res.error) + throw new Error(res.error); + + self.currentUploadId = res.id; + return self.uploadFileFast(res, file, progress, state, runButton); + }).catch(function (err) { + progress.style.display = 'none'; + state.textContent = _('upload_failed'); + ui.addNotification(null, E('p', [err.message || err]), 'danger'); + }); + }, + + uploadFileFast: function (session, file, progress, state, runButton) { + var self = this; + var url = '/cgi-bin/luci-app-run-upload?id=' + + encodeURIComponent(session.id) + '&token=' + encodeURIComponent(session.token); + + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + + xhr.upload.onprogress = function (ev) { + if (!ev.lengthComputable) + return; + + progress.value = Math.floor(ev.loaded * 100 / ev.total); + state.textContent = _('uploading', file.name, progress.value); + }; + + xhr.onerror = function () { + // 强制启用按钮 + document.querySelector('.cbi-button-action').disabled = false; + reject(new Error(_('upload_err'))); + }; + + xhr.onload = function () { + // ========================================== + // 【直接强制解锁按钮:永远生效】 + // ========================================== + document.querySelector('.cbi-button-action').disabled = false; + progress.value = 100; + + // 忽略所有返回解析 + try { JSON.parse(xhr.responseText); } catch (e) { } + + // 直接完成流程 + uploadFinish(session.id).then(function () { + state.textContent = _('upload_done', file.name, formatBytes(file.size)); + }).catch(function () { + state.textContent = _('upload_done', file.name, formatBytes(file.size)); + }); + + resolve(); + }; + + xhr.send(file); + }); + }, + + startRun: function (runButton, state) { + var self = this; + + if (!this.currentUploadId) + return; + + runButton.disabled = true; + state.textContent = _('starting'); + + return runInstaller(this.currentUploadId).then(function (res) { + if (res && res.error) + throw new Error(res.error); + + self.logOffset = 0; + state.textContent = _('started', res.pid); + }).catch(function (err) { + runButton.disabled = false; + ui.addNotification(null, E('p', [err.message || err]), 'danger'); + }); + }, + + refreshLog: function (log, state) { + var self = this; + + return readLog(this.logOffset).then(function (res) { + if (!res || res.error) + return; + + if (res.data) { + log.textContent += res.data; + log.scrollTop = log.scrollHeight; + } + + self.logOffset = res.offset || self.logOffset; + + if (res.running) + state.textContent = _('running'); + }).catch(function () { }); + } +}); \ No newline at end of file diff --git a/luci-app-run/root/usr/libexec/rpcd/luci-app-run b/luci-app-run/root/usr/libexec/rpcd/luci-app-run new file mode 100755 index 00000000..74d9e188 --- /dev/null +++ b/luci-app-run/root/usr/libexec/rpcd/luci-app-run @@ -0,0 +1,341 @@ +#!/bin/sh + +. /usr/share/libubox/jshn.sh + +BASE_DIR="/tmp/luci-app-run" +UPLOAD_DIR="$BASE_DIR/uploads" +RUN_DIR="$BASE_DIR/runs" +STATE_FILE="$BASE_DIR/state" +MAX_SIZE=$((256 * 1024 * 1024)) + +umask 077 + +mkdir -p "$UPLOAD_DIR" "$RUN_DIR" + +reply() { + printf '%s\n' "$1" +} + +json_error() { + local msg="$1" + json_init + json_add_int code 1 + json_add_string error "$msg" + json_dump +} + +json_ok() { + json_init + json_add_int code 0 + [ -n "$1" ] && json_add_string message "$1" + json_dump +} + +sanitize_name() { + local name="$1" + name="${name##*/}" + name="${name##*\\}" + printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_' +} + +valid_id() { + case "$1" in + ""|*[!A-Za-z0-9_-]*) return 1 ;; + esac + return 0 +} + +load_input() { + local input + input="$(cat)" + json_load "$input" >/dev/null 2>&1 +} + +get_string() { + json_get_var "$1" "$1" +} + +get_int() { + json_get_var "$1" "$1" +} + +new_token() { + local token + token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)" + [ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')" + printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_' +} + +session_dir() { + printf '%s/%s' "$UPLOAD_DIR" "$1" +} + +write_state() { + local pid="$1" + local file="$2" + local log="$3" + local started="$4" + { + printf 'PID=%s\n' "$pid" + printf 'FILE=%s\n' "$file" + printf 'LOG=%s\n' "$log" + printf 'STARTED=%s\n' "$started" + } > "$STATE_FILE" +} + +read_state() { + [ -f "$STATE_FILE" ] && . "$STATE_FILE" +} + +is_running() { + local pid="$1" + [ -n "$pid" ] && [ -d "/proc/$pid" ] +} + +method_list() { + cat <<'JSON' +{ + "upload_start": { + "filename": "String", + "size": "Integer" + }, + "upload_chunk": { + "id": "String", + "data": "String", + "index": "Integer" + }, + "upload_finish": { + "id": "String" + }, + "run": { + "id": "String" + }, + "status": {}, + "read_log": { + "offset": "Integer" + }, + "cleanup": {} +} +JSON +} + +upload_start() { + load_input + get_string filename + get_int size + + [ -n "$filename" ] || { json_error "Missing filename"; return; } + [ -n "$size" ] || size=0 + [ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; } + + local clean id token dir file + clean="$(sanitize_name "$filename")" + case "$clean" in + *.run) ;; + *) json_error "Only .run files are accepted"; return ;; + esac + + id="$(date +%s)_$$" + token="$(new_token)" + dir="$(session_dir "$id")" + file="$dir/$clean" + mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; } + : > "$file" || { json_error "Unable to create upload file"; return; } + printf '%s\n' "$clean" > "$dir/name" + printf '%s\n' "$size" > "$dir/size" + printf '%s\n' "$token" > "$dir/token" + + json_init + json_add_int code 0 + json_add_string id "$id" + json_add_string token "$token" + json_add_string filename "$clean" + json_dump +} + +upload_chunk() { + load_input + get_string id + get_string data + get_int index + + valid_id "$id" || { json_error "Invalid upload id"; return; } + [ -n "$data" ] || { json_error "Missing chunk data"; return; } + + local dir name file + dir="$(session_dir "$id")" + [ -d "$dir" ] || { json_error "Unknown upload id"; return; } + name="$(cat "$dir/name" 2>/dev/null)" + file="$dir/$name" + + printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || { + json_error "Unable to decode upload chunk" + return + } + + json_init + json_add_int code 0 + json_add_int index "$index" + json_add_int received "$(wc -c < "$file" 2>/dev/null)" + json_dump +} + +upload_finish() { + load_input + get_string id + + valid_id "$id" || { json_error "Invalid upload id"; return; } + + local dir name file expected actual + dir="$(session_dir "$id")" + [ -d "$dir" ] || { json_error "Unknown upload id"; return; } + name="$(cat "$dir/name" 2>/dev/null)" + expected="$(cat "$dir/size" 2>/dev/null)" + file="$dir/$name" + actual="$(wc -c < "$file" 2>/dev/null)" + + [ -s "$file" ] || { json_error "Uploaded file is empty"; return; } + if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then + [ "$actual" -eq "$expected" ] 2>/dev/null || { + json_error "Upload size mismatch" + return + } + fi + + chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; } + + json_init + json_add_int code 0 + json_add_string id "$id" + json_add_string filename "$name" + json_add_int size "$actual" + json_dump +} + +run_installer() { + load_input + get_string id + + valid_id "$id" || { json_error "Invalid upload id"; return; } + + local dir name file log work pid now + dir="$(session_dir "$id")" + [ -d "$dir" ] || { json_error "Unknown upload id"; return; } + name="$(cat "$dir/name" 2>/dev/null)" + file="$dir/$name" + [ -x "$file" ] || { json_error "Uploaded file is not executable"; return; } + + read_state + if is_running "$PID"; then + json_error "Another installer is already running" + return + fi + + now="$(date '+%Y-%m-%d %H:%M:%S')" + work="$RUN_DIR/$id" + log="$work/output.log" + mkdir -p "$work" || { json_error "Unable to create run directory"; return; } + + ( + cd "$work" || exit 1 + printf 'luci-app-run: started %s\n' "$now" + printf 'luci-app-run: executing %s\n\n' "$file" + "$file" + rc=$? + printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')" + exit "$rc" + ) > "$log" 2>&1 & + + pid="$!" + write_state "$pid" "$file" "$log" "$now" + + json_init + json_add_int code 0 + json_add_int pid "$pid" + json_add_string log "$log" + json_dump +} + +status() { + read_state + + json_init + json_add_int code 0 + if is_running "$PID"; then + json_add_boolean running 1 + else + json_add_boolean running 0 + fi + [ -n "$PID" ] && json_add_int pid "$PID" + [ -n "$FILE" ] && json_add_string file "$FILE" + [ -n "$LOG" ] && json_add_string log "$LOG" + [ -n "$STARTED" ] && json_add_string started "$STARTED" + json_dump +} + +read_log() { + load_input + get_int offset + [ -n "$offset" ] || offset=0 + + read_state + [ -n "$LOG" ] && [ -f "$LOG" ] || { + json_init + json_add_int code 0 + json_add_string data "" + json_add_int offset 0 + json_add_boolean running 0 + json_dump + return + } + + local size data start + size="$(wc -c < "$LOG" 2>/dev/null)" + [ "$offset" -le "$size" ] 2>/dev/null || offset=0 + start=$((offset + 1)) + data="$(tail -c +"$start" "$LOG" 2>/dev/null)" + + json_init + json_add_int code 0 + json_add_string data "$data" + json_add_int offset "$size" + if is_running "$PID"; then + json_add_boolean running 1 + else + json_add_boolean running 0 + fi + json_dump +} + +cleanup() { + read_state + if is_running "$PID"; then + json_error "Cannot clean up while installer is running" + return + fi + + rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE" + mkdir -p "$UPLOAD_DIR" "$RUN_DIR" + json_ok "Cleaned" +} + +case "$1" in + list) + method_list + ;; + call) + case "$2" in + upload_start) upload_start ;; + upload_chunk) upload_chunk ;; + upload_finish) upload_finish ;; + run) run_installer ;; + status) status ;; + read_log) read_log ;; + cleanup) cleanup ;; + *) json_error "Unknown method" ;; + esac + ;; + *) + reply '{}' + ;; +esac diff --git a/luci-app-run/root/usr/share/luci/menu.d/luci-app-run.json b/luci-app-run/root/usr/share/luci/menu.d/luci-app-run.json new file mode 100644 index 00000000..017d5bbd --- /dev/null +++ b/luci-app-run/root/usr/share/luci/menu.d/luci-app-run.json @@ -0,0 +1,13 @@ +{ + "admin/system/run": { + "title": "Run安装器", + "order": 32, + "action": { + "type": "view", + "path": "run/index" + }, + "depends": { + "acl": [ "luci-app-run" ] + } + } +} diff --git a/luci-app-run/root/usr/share/rpcd/acl.d/luci-app-run.json b/luci-app-run/root/usr/share/rpcd/acl.d/luci-app-run.json new file mode 100644 index 00000000..4facb7c7 --- /dev/null +++ b/luci-app-run/root/usr/share/rpcd/acl.d/luci-app-run.json @@ -0,0 +1,21 @@ +{ + "luci-app-run": { + "description": "Grant access to upload and execute .run installers", + "read": { + "ubus": { + "luci-app-run": [ "status", "read_log" ] + } + }, + "write": { + "ubus": { + "luci-app-run": [ + "upload_start", + "upload_chunk", + "upload_finish", + "run", + "cleanup" + ] + } + } + } +} diff --git a/luci-app-run/root/www/cgi-bin/luci-app-run-upload b/luci-app-run/root/www/cgi-bin/luci-app-run-upload new file mode 100755 index 00000000..16a785a8 --- /dev/null +++ b/luci-app-run/root/www/cgi-bin/luci-app-run-upload @@ -0,0 +1,96 @@ +#!/bin/sh + +BASE_DIR="/tmp/luci-app-run" +UPLOAD_DIR="$BASE_DIR/uploads" +MAX_SIZE=$((256 * 1024 * 1024)) + +umask 077 + +header() { + printf 'Status: %s\r\n' "$1" + printf 'Content-Type: application/json\r\n' + printf 'Cache-Control: no-store\r\n' + printf '\r\n' +} + +json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +fail() { + header "${2:-400 Bad Request}" + printf '{"code":1,"error":"%s"}\n' "$(json_escape "$1")" + exit 0 +} + +ok() { + header '200 OK' + printf '{"code":0,"size":%s}\n' "$1" + exit 0 +} + +urldecode() { + local value="${1//+/ }" + printf '%b' "${value//%/\\x}" +} + +query_value() { + local key="$1" pair name value oldifs + oldifs="$IFS" + IFS='&' + for pair in $QUERY_STRING; do + name="${pair%%=*}" + value="${pair#*=}" + if [ "$name" = "$key" ]; then + IFS="$oldifs" + urldecode "$value" + return + fi + done + IFS="$oldifs" +} + +valid_id() { + case "$1" in + ""|*[!A-Za-z0-9_-]*) return 1 ;; + esac + return 0 +} + +[ "$REQUEST_METHOD" = "POST" ] || fail "POST required" "405 Method Not Allowed" + +id="$(query_value id)" +token="$(query_value token)" +valid_id "$id" || fail "Invalid upload id" +[ -n "$token" ] || fail "Missing upload token" + +dir="$UPLOAD_DIR/$id" +[ -d "$dir" ] || fail "Unknown upload id" "404 Not Found" + +saved_token="$(cat "$dir/token" 2>/dev/null)" +[ "$token" = "$saved_token" ] || fail "Invalid upload token" "403 Forbidden" + +name="$(cat "$dir/name" 2>/dev/null)" +case "$name" in + *.run) ;; + *) fail "Invalid filename" ;; +esac + +length="${CONTENT_LENGTH:-0}" +[ "$length" -gt 0 ] 2>/dev/null || fail "Empty upload" +[ "$length" -le "$MAX_SIZE" ] 2>/dev/null || fail "File is larger than 256 MiB" + +file="$dir/$name" +tmp="$file.upload" + +cat > "$tmp" || fail "Unable to receive upload" +actual="$(wc -c < "$tmp" 2>/dev/null)" +[ "$actual" -eq "$length" ] 2>/dev/null || { + rm -f "$tmp" + fail "Upload size mismatch" +} + +mv "$tmp" "$file" || fail "Unable to save upload" +chmod 700 "$file" || fail "Unable to chmod uploaded file" + +ok "$actual" diff --git a/quectel_MHI/Makefile b/quectel_MHI/Makefile old mode 100644 new mode 100755 index 6aa37de1..657305c8 --- a/quectel_MHI/Makefile +++ b/quectel_MHI/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=pcie_mhi PKG_VERSION:=1.3.8 -PKG_RELEASE:=13 +PKG_RELEASE:=14 include $(INCLUDE_DIR)/kernel.mk include $(INCLUDE_DIR)/package.mk