想着给博客做一下国际化支持，为每篇文章和每个页面单独配置一个英文版本，但是上网查了一圈发现Typecho对国际化的支持并不好，最终自己设计了一套方案，写这篇文章记录下来 本文默认你对PHP、Nginx、Typecho的基础逻辑有部分了解 分析 需求 需要为每个文章和页面都配置中英双语 需要配置一个语言选择器，使得前端可以快速切换语言 需要搜索引擎可以正确识别并收录多语言版本的文章 大致方案 中英文文章的区分大致有这两种方案： 作为一个单独的参数，如访问文章时用/?lang=zh-CN和/?lang=en-US来区分，但是这种方案实现起来比较困难，同时对搜索引擎收录也不太好； 通过URL路径区分，如访问https://<host>/article是中文页面，访问https://<host>/en/article是英文页面，这种配置起来比较简单，直接分成两个Typecho实例即可，并且对搜索引擎友好。问题是评论和浏览量统计需要手动同步。 总结下来，我选择了第二种方案，并且打算直接通过在/en/新建一个Typecho实例实现多语言支持。 具体实现 首先将博客实例复制两份，一份中文一份英文，然后分别翻译成英文； 修改前端代码，实现语言选择器； 要保证两边文章URL只有一个/en之差，就得保证中文站点和英文站点文章的cid相同，而这个cid是根据创建文章和附件的顺序来的，因此打算编写一个同步插件，中文端发文章的时候自动在英文数据库中插入一条对应cid的文章； 改写SiteMap插件，因为sitemap不能既包含页面链接又包含其他sitemap的引用，因此主站需要创建两个sitemap，一个是收录中文站页面的主sitemap，另一个是索引，负责索引中文和英文的sitemap； 在 中添加hreflang属性来告知搜索引擎如何处理多语言； 将英文端的访问量和点赞量链接到中文的数据库； 将英文端的评论链接到中文数据库。 开整 创建英文实例 将整个网站复制一份，放在原先的网站根目录的/en/文件夹下，同时将数据库也复制一份，我这里命名成typecho_en。 接着，分别为两个实例配置伪静态： location /en/ { if (!-e $request_filename) { rewrite ^(.*)$ /en/index.php$1 last; } } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } 关于为什么要将中文的主实例也用location括起来，是因为实际测试的时候发现如果不括起来就会将英文实例也当作中文实例的一部分解析，造成404。 同时，修改<webroot>/en/config.inc.php里的数据库配置，指向英文实例的数据库。 这个时候，访问<host>/en/应该能看到一个和中文主站一模一样的站点。 修改Typecho语言 这一步其实感觉可以不要，因为基本上前端页面的语言都是由主题决定的，将Typecho本身语言设置为英文没什么大用，但是为了统一，还是选择改了一下（这样可以一眼看出来是哪个实例的后台（笑）） 直接参考GitHub上typecho官方的多语言支持即可，从Release里下载语言包，然后解压到<webroot>/en/usr/langs/下，然后进入https://<host>/en/admin/options-general.php，就能看见语言设置选项，改为English即可。 修改主题语言 最繁琐的一步，我使用的是Joe，进入<webroot>/en/usr/themes/Joe，将所有你能看到的显示相关的中文都翻译成英文，这里没什么非常方便的方法，用翻译工具的话翻出来会很别扭，我还是选择了手动翻译。 需要注意的是，有一部分的前端配置是在js中的，不止是PHP源文件，这些都需要翻译。 翻译文章 这一步没啥好说的，把/en/下的文章一篇篇翻译成英文，保存 配置文章同步发布 这一步是为了保持两边文章的cid同步，因为cid和访问url有关，保持cid同步可以使下文配置语言选择器一步更加简单，只要在host后加个/en或者去掉就行了。 cid是数据库中typecho_contents表的一个自增字段，同时也是主键，他的分配和typecho的文章、附件等有关。因为上传的文件也会占用cid，而我打算将所有附件都上传到中文站，因此如果不做特殊处理两边的cid容易匹配不上，增加后续工作量。 因此我选择的方案是用AI编写一个插件，当中文站发布文章的时候自动触发，读取中文站为其分配的cid并写入到英文站的数据库中。 创建文件<webroot>/usr/plugins/SyncToEnglish/Plugin.php并填入如下内容: <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * 中文文章同步到英文数据库 * * @package SyncToEnglish * @author ChatGPT, iYoRoy * @version 1.0.0 * @link https://example.com */ class SyncToEnglish_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = [__CLASS__, 'push']; return 'SyncToEnglish 插件已启用：发布中文文章时会自动在英文库创建对应空文章'; error_log("[SyncToEnglish] 插件激活成功"); } public static function deactivate() { return 'SyncToEnglish 插件已禁用'; } public static function config(Typecho_Widget_Helper_Form $form) { $host = new Typecho_Widget_Helper_Form_Element_Text('host', NULL, 'localhost', _t('英文数据库主机')); $user = new Typecho_Widget_Helper_Form_Element_Text('user', NULL, 'root', _t('英文数据库用户名')); $password = new Typecho_Widget_Helper_Form_Element_Password('password', NULL, NULL, _t('英文数据库密码')); $database = new Typecho_Widget_Helper_Form_Element_Text('database', NULL, 'typecho_en', _t('英文数据库名称')); $port = new Typecho_Widget_Helper_Form_Element_Text('port', NULL, '3306', _t('英文数据库端口')); $charset = new Typecho_Widget_Helper_Form_Element_Text('charset', NULL, 'utf8mb4', _t('字符集')); $prefix = new Typecho_Widget_Helper_Form_Element_Text('prefix', NULL, 'typecho_', _t('表前缀')); $form->addInput($host); $form->addInput($user); $form->addInput($password); $form->addInput($database); $form->addInput($port); $form->addInput($charset); $form->addInput($prefix); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function push($contents, $widget) { $options = Helper::options(); $config = $options->plugin('SyncToEnglish'); // 获取中文库文章信息 $cnDb = Typecho_Db::get(); if (is_array($contents) && isset($contents['cid'])) { $cid = $contents['cid']; $title = $contents['title']; } elseif (is_object($contents) && isset($contents->cid)) { $cid = $contents->cid; $title = $contents->title; } else { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.contents')->order('cid', Typecho_Db::SORT_DESC)->limit(1)); $cid = $row['cid']; $title = $row['title']; error_log("[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n", 3, __DIR__ . '/debug.log'); } $article = $cnDb->fetchRow($cnDb->select()->from('table.contents')->where('cid = ?', $cid)); if (!$article) return; $enDb = new Typecho_Db('Mysql', $config->prefix); $enDb->addServer([ 'host' => $config->host, 'user' => $config->user, 'password' => $config->password, 'charset' => $config->charset, 'port' => (int)$config->port, 'database' => $config->database ], Typecho_Db::READ | Typecho_Db::WRITE); try { $exists = $enDb->fetchRow($enDb->select()->from('table.contents')->where('cid = ?', $article['cid'])); if ($exists) { $enDb->query($enDb->update('table.contents') ->rows([ // 'title' => $article['title'], 'slug' => $article['slug'], 'modified' => $article['modified'] ]) ->where('cid = ?', $article['cid']) ); } else { $enDb->query($enDb->insert('table.contents')->rows([ 'cid' => $article['cid'], 'title' => $article['title'], 'slug' => $article['slug'], 'created' => $article['created'], 'modified' => $article['modified'], 'type' => $article['type'], 'status' => $article['status'], 'authorId' => $article['authorId'], 'views' => 0, 'text' => $article['text'], 'allowComment' => $article['allowComment'], 'allowFeed' => $article['allowFeed'], 'allowPing' => $article['allowPing'] ])); } } catch (Exception $e) { error_log('[SyncToEnglish] 同步失败: ' . $e->getMessage()); } } } 随后去后台启用插件并配置上英文数据库的数据库信息即可。 完成后，在中文站发布文章，英文站应该能同步发布一篇相同cid的文章。 配置语言选择器 既然我们已经完成了同步文章cid，接下来我们修改语言的时候只需要修改url，在前面加上或者去掉/en/就行了。通过PHP编写一个选择器，放在主题的顶栏中： Language <path d="M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z" fill="var(--main)" /> <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo '简体中文'; echo 'English'; ?> 需要在中文和英文两个实例中都添加。 之后，全局都能通过这个选择器选择语言。对于我用的Joe主题，移动端和PC端需要单独编写两个语言选择器。 改写SiteMap插件 为了让搜索引擎能更快的收录到英文的页面，我打算修改SiteMap插件，使之包含英文站点的页面。SiteMap有两种，一种是sitemapindex，用于索引子SiteMap，一种是urlset，用于包含页面。我使用的是joyqi/typecho-plugin-sitemap插件，在这个基础上，将默认的/sitemap.xml改为sitemapindex，新建一个路由/sitemap_cn.xml来存放中文站的SiteMap，英文站点的插件不变，再有默认的/sitemap.xml引用/sitemap_cn.xml和/en/sitemap.xml。 修改SiteMap的Plugin.php： /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( - 'sitemap', + 'sitemap_index', '/sitemap.xml', Generator::class, - 'generate', + 'generate_index', 'index' ); + Helper::addRoute( + 'sitemap_cn', + '/sitemap_cn.xml', + Generator::class, + 'generate_cn', + 'index' + ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { - Helper::removeRoute('sitemap'); + Helper::removeRoute('sitemap_index'); + Helper::removeRoute('sitemap_cn'); } {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Utils\Helper; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * 自动生成 Typecho 站点地图的插件。 * 站点地图 Sitemap 的地址是：http(s)://yourdomain.com/sitemap.xml * * @package 站点地图插件 * @author joyqi * @version 1.0.0 * @since 1.2.1 * @link https://github.com/joyqi/typecho-plugin-sitemap */ class Plugin implements PluginInterface { /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( 'sitemap_index', '/sitemap.xml', Generator::class, 'generate_index', 'index' ); Helper::addRoute( 'sitemap_cn', '/sitemap_cn.xml', Generator::class, 'generate_cn', 'index' ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { Helper::removeRoute('sitemap_index'); Helper::removeRoute('sitemap_cn'); } /** * Plugin config panel render method. * * @param Form $form */ public static function config(Form $form) { $sitemapBlock = new Form\Element\Checkbox( 'sitemapBlock', [ 'posts' => _t('生成文章链接'), 'pages' => _t('生成独立页面链接'), 'categories' => _t('生成分类链接'), 'tags' => _t('生成标签链接'), ], ['posts', 'pages', 'categories', 'tags'], _t('站点地图显示') ); $updateFreq = new Form\Element\Select( 'updateFreq', [ 'daily' => _t('每天'), 'weekly' => _t('每周'), 'monthly' => _t('每月或更久'), ], 'daily', _t('更新频率') ); // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text('externalSitemap', NULL, '', _t('附加SiteMap')); $form->addInput($sitemapBlock->multiMode()); $form->addInput($updateFreq); // $form->addInput($externalSitemap); } /** * Plugin personal config panel render method. * * @param Form $form */ public static function personalConfig(Form $form) { // TODO: Implement personalConfig() method. } } {/collapse-item} {/collapse} 修改SiteMap的Generator.php： class Generator extends Contents { + public function generate_index(){ + $sitemap = '<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <sitemap> + <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> + </sitemap> + <sitemap> + <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> + </sitemap> +</sitemapindex>'; + $this->response->throwContent($sitemap, 'text/xml'); + } + /** * @return void */ - public function generate() + public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; ... {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Widget\Base\Contents; use Widget\Contents\Page\Rows; use Widget\Contents\Post\Recent; use Widget\Metas\Category\Rows as CategoryRows; use Widget\Metas\Tag\Cloud; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Sitemap Generator */ class Generator extends Contents { public function generate_index(){ $sitemap = '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex>'; $this->response->throwContent($sitemap, 'text/xml'); } /** * @return void */ public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"' . ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' . ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' . ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . PHP_EOL; // add homepage $sitemap .= <<<EOF <url> <loc>{$this->options->siteUrl}</loc> <changefreq>daily</changefreq> <priority>1.0</priority> </url> EOF; // add posts if (in_array('posts', $this->options->plugin('Sitemap')->sitemapBlock)) { $postsCount = $this->size($this->select() ->where('table.contents.status = ?', 'publish') ->where('table.contents.created < ?', $this->options->time) ->where('table.contents.type = ?', 'post')); $posts = Recent::alloc(['pageSize' => $postsCount]); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'monthly' : 'weekly'; while ($posts->next()) { $sitemap .= <<<EOF <url> <loc>{$posts->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$posts->date->format('c')}</lastmod> <priority>0.8</priority> </url> EOF; } } // add pages if (in_array('pages', $this->options->plugin('Sitemap')->sitemapBlock)) { $pages = Rows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'yearly' : 'monthly'; while ($pages->next()) { $sitemap .= <<<EOF <url> <loc>{$pages->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$pages->date->format('c')}</lastmod> <priority>0.5</priority> </url> EOF; } } // add categories if (in_array('categories', $this->options->plugin('Sitemap')->sitemapBlock)) { $categories = CategoryRows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($categories->next()) { $sitemap .= <<<EOF <url> <loc>{$categories->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.6</priority> </url> EOF; } } // add tags if (in_array('tags', $this->options->plugin('Sitemap')->sitemapBlock)) { $tags = Cloud::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($tags->next()) { $sitemap .= <<<EOF <url> <loc>{$tags->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.4</priority> </url> EOF; } } $sitemap .= '</urlset>'; $this->response->throwContent($sitemap, 'text/xml'); } } {/collapse-item} {/collapse} 请将代码中的博客地址改成你自己的 （最近太忙了实在是没空给单独写个配置页面了就直接把SiteMap URL写死在插件里了） 禁用再启用插件，访问https://<host>/sitemap.xml应该就能看到SiteMap的索引了： <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex> 通过Bing Search等其他搜索引擎的SEO管理页应该也能看到扫描到了英文站的SiteMap： 添加hreflang 这一步的作用是让搜索引擎知道当前页面存在多语言版本，使其可以根据用户的语言偏好或地理位置展示合适的页面。我们需要在页面的 中插入类似如下格式的link标签： 其中，hreflang="x-default"意为本文的默认语言。hreflang的值是由ISO 639-1的语言代码和ISO 3166-1 Alpha-2的区域代码构成的，其中区域代码可以省略，只标识语言（类似于中文和简体中文（大陆—）、繁体中文（香港），繁体中文（台湾）的区别）。 在主题的 相关配置中加入如下内容： <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo ' '; echo ' '; echo ' '; ?> 同时需要在中文和英文站点都添加一遍。 之后，访问我们的网站，应该就能在 中找到相应的hreflang配置。 同步点赞量和浏览量 这一步和主题关联性比较大，可能并不适用于所有主题。我用的Joe主题，由主题本身连接数据库并读取写入点赞量、浏览量。我直接通过修改英文实例的主题代码，使其直接读写中文实例的点赞量和浏览量即可。 修改<webroot>/en/usr/themes/Joe/core/function.php中获取浏览量的函数： /* 查询文章浏览量 */ function _getViews($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $item->cid))['views']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/function.php中获取点赞量的函数： /* 查询文章点赞量 */ function _getAgree($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $item->cid))['agree']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/route.php中主页显示浏览量部分的代码： $result[] = array( "mode" => $item->fields->mode ? $item->fields->mode : 'default', "image" => _getThumbnails($item), "time" => date('Y-m-d', $item->created), "created" => date('d/m/Y', $item->created), "title" => $item->title, "abstract" => _getAbstract($item, false), "category" => $item->categories, - "views" => number_format($item->views), + // "views" => number_format($item->views), + "views" => _getViews($item, false), "commentsNum" => number_format($item->commentsNum), - "agree" => number_format($item->agree), + // "agree" => number_format($item->agree), + "agree" => _getAgree($item, false), "permalink" => $item->permalink, "lazyload" => _getLazyload(false), "type" => "normal" ); 文章页面中显示浏览量部分的代码本身就是调用的_getViews，因此不需要修改。 修改增加浏览量部分的代码： /* 增加浏览量 已测试 √ */ function _handleViews($self) { $self->response->setStatus(200); $cid = $self->request->cid; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 修改点赞和取消点赞部分代码： /* 点赞和取消点赞 已测试 √ */ function _handleAgree($self) { $self->response->setStatus(200); $cid = $self->request->cid; $type = $self->request->type; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } /* sql注入校验 */ if (!preg_match('/^[agree|disagree]+$/', $type)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 完成上述修改后保存，接着访问英文站可发现和中文站的浏览量同步了。 同步评论 本来想弄个插件，hook一下发布评论的函数，在发布的时候同时在另一个实例的数据库中同步插入评论信息，但是发现我的Joe主体已经hook了，我再hook会冲突，因此直接编辑Joe主题的代码： 编辑<webroot>/index/usr/themes/Joe/core/factory.php： <?php require_once("phpmailer.php"); require_once("smtp.php"); /* 加强评论拦截功能 */ Typecho_Plugin::factory('Widget_Feedback')->comment = array('Intercept', 'message'); class Intercept { public static function message($comment) { ... Typecho_Cookie::delete('__typecho_remember_text'); + + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho_en', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho_en' + ], Typecho_Db::READ | Typecho_Db::WRITE); + + $row = [ + 'coid' => $comment['coid'], // 必须包含新生成的评论ID + 'cid' => $comment['cid'], + 'created' => $comment['created'], + 'author' => $comment['author'], + 'authorId' => $comment['authorId'], + 'ownerId' => $comment['ownerId'], + 'mail' => $comment['mail'], + 'url' => $comment['url'], + 'ip' => $comment['ip'], + 'agent' => $comment['agent'], + 'text' => $comment['text'], + 'type' => $comment['type'], + 'status' => $comment['status'], + 'parent' => $comment['parent'] + ]; + + // 插入数据到目标数据库的 `comments` 表 + $db->query($db->insert('typecho_comments')->rows($row)); return $comment; } } ... 同理也在英文实例如此操作一番，插入到中文的数据库中 这个方案存在的一个问题就是如果有垃圾评论需要删除，需要分别去两个实例各删除一次，后面再修吧（x 参考文章： typecho/languages - GitHub
前情提要 上一篇文章实现了统一的日志管理，这篇文章来实现进程间消息通讯，即IPC。 分析 Windows 在Windows下，主要通过管道（Pipe）实现进程间通讯。管道又分为命名管道（Named Pipe）和匿名管道（Anonymous Pipe）。其中，匿名管道是单向的，通常在父进程和子进程之间通讯[2]。而命名管道则可以是单向管道或双工管道，并且支持一对多通讯[3]。顾名思义，识别命名管道的唯一方式是它的名称，因此两个进程只要都连接到同一个名字的命名管道即可实现通信。 我们需要实现进程间的双向通讯，因此采用命名管道。大致思路就是：进程作为伺服模式，也就是接收端启动时创建一个线程，创建一个命名管道并监听管道内消息。当管道被连接时从中读取管道内数据；当进程作为发送端启动时尝试连接到同一个名称的管道，并写入消息内容。 Linux 在Linux下通常使用socket进行进程间通讯。不过不同于监听端口，进程间通讯一般会选择监听一个sock文件[5]，常见的服务类应用如docker daemon、mysql都是通过这种方式。 因此，大致思路如下：作为伺服模式启动的进程创建一个socket监听，并等待从中接收消息；发送端连接到socket套接字并发送消息。和上文命名管道的名称类似，socket套接字会映射一个唯一的.sock文件，发送方只要打开这个文件即可发送消息。（实际上打开方式不是常规的打开文件，而是用socket专用的打开方式[5]） 代码实现 初始化 为了实现共用一套主代码，我使用了和上一篇文章中一样的通过宏定义区分系统类型的方案，将Windows和Linux的代码分别写在service-windows.h和service-linux.h两个头文件中： #ifdef _WIN32 #include "service-windows.h" #elif defined(__linux__) #include "service-linux.h" #endif 当接收端进程启动时，创建一个线程处理收信息（使用std::thread作为多线程库）： thread_bind = std::thread(bind_thread_main); 监听部分 Windows 在Windows下，只要尝试从指定名称的命名管道读取数据即可。其中，因为设置了管道为等待模式（即下文中CreateNamedPipe的第三个参数DWORD dwPipeMode中设置了PIPE_WAIT），ConnectNamedPipe会是阻塞模式，因此不用担心不断循环造成的性能损失。 void bind_thread_main() { while (!exit_requested.load()) { HANDLE hPipe = CreateNamedPipe( PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, // Output buffer size 1024, // Input buffer size 0, // Default timeout NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_WARN, "Failed to create pipe: %d", GetLastError()); continue; } if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) { char buffer[1024]; DWORD bytesRead; if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) { buffer[bytesRead] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } FlushFileBuffers(hPipe); DisconnectNamedPipe(hPipe); CloseHandle(hPipe); } else { CloseHandle(hPipe); } } } Linux 为了防止创建失败，在创建前会先尝试删除没有清理干净的sock文件，即代码中unlink(SOCKET_PATH)。SOCKET_PATH为全局变量，定义了套接字文件的路径。创建套接字时，指定family为AF_UNIX代表创建UNIX套接字，即.sock文件的这种类型（如果是网络套接字就是AF_INET）。timeval这段代码设置了一个超时限制，当accept函数等待时间超过设置的SOCKET_TIMEOUT超时时间（单位秒）后会自动结束阻塞并返回错误信息。创建完套接字后按照正常流程设置绑定和监听即可。 void bind_thread_main() { unlink(SOCKET_PATH); int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd == -1) { service_log.push(LEVEL_FATAL, "Failed to create socket"); exit_requested.store(true); return; } struct timeval tv; tv.tv_sec = SOCKET_TIMEOUT; tv.tv_usec = 0; setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_FATAL, "Bind failed"); close(server_fd); exit_requested.store(true); return; } if (listen(server_fd, 5) == -1) { service_log.push(LEVEL_FATAL, "Listen failed"); close(server_fd); exit_requested.store(true); return; } while (!exit_requested.load()) { int client_fd = accept(server_fd, nullptr, nullptr); if (client_fd != -1) { char buffer[1024]; int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } close(client_fd); } else { if (errno == EWOULDBLOCK || errno == EAGAIN) { continue; } service_log.push(LEVEL_WARN, "Failed to accept socket connection"); } } } 当读取到消息后，两份代码都会将消息保存至阻塞队列m_queueMsg中。 发送部分 Windows 打开指定管道并写入消息内容即可： bool send_message(const std::string& msg) { if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) { service_log.push(LEVEL_ERROR, "Failed to find valid pipe: %d", GetLastError()); return false; } HANDLE hPipe = CreateFile( PIPE_NAME, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_ERROR, "Failed to connect: %d", GetLastError()); return false; } DWORD bytesWritten; if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &bytesWritten, NULL)) { service_log.push(LEVEL_VERBOSE, "Message sent: %s", msg.c_str()); CloseHandle(hPipe); return true; } else { service_log.push(LEVEL_ERROR, "Message (%s) send failed: %d", msg.c_str(),GetLastError()); CloseHandle(hPipe); return false; } } Linux 同理，连接套接字，发送数据： bool send_message(const std::string& msg) { int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { service_log.push(LEVEL_ERROR, "Failed to create socket"); return false; } sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_ERROR, "Connect failed"); close(sock); return false; } if (write(sock, msg.c_str(), msg.size()) == -1) { service_log.push(LEVEL_ERROR, "Message send failed: %s", msg.c_str()); close(sock); return false; } else { service_log.push(LEVEL_VERBOSE, "Message sent success: %s", msg.c_str()); close(sock); return true; } } 清理 Windows下没什么需要清理的，Linux下删除套接字文件即可： unlink(SOCKET_PATH); 效果示意图 Windows Linux 样例代码下载： IPCTest.zip 参考文章： https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes https://www.cnblogs.com/alantu2018/p/8493809.html https://blog.csdn.net/dog250/article/details/100998838
跨平台服务编写日记 Ep.1 统一的日志管理
前阵子心血来潮，想为在使用的一个跨平台的控制台服务类应用程序编写一个自己的管理程序，加一些功能。因而设计了一套简单的服务运行流程。 {alert type="warning"} 本系列文章中的观点和方案为我根据自己已有的知识储备结合DeepSeek的帮助所归纳设计，并未经过严格测试，并不保证在生产环境中使用的可行性和稳定性 {/alert} 大致思路 大致分为个线程，分别用作： 日志记录 目标应用实例管理，可能不止一个线程 监听IPC消息 处理IPC收到的消息（主进程） 本文着重讨论的是日志记录部分。 编写思路 为什么要给日志记录单开一个线程，个人考虑是因为本身就是多线程的架构，需要编写一个统一的日志记录模块。如果每个线程单独打印，则很有可能出现两个线程同时写入文件或者同时输出到控制台，造成日志混乱。 因此，日志记录大致思路就是： 定义一个队列，存储日志内容和等级 创建一个线程，不断地从线程中取出元素，根据设定的日志等级决定是否打印到控制台或者输出到文件 外部push日志内容到队列中 一些细节上的内容 保证可移植性，尽量使用STL库编写，如使用std::thread而不是pthread 保证线程安全，需要使用互斥锁之类的保护相应变量 让日志队列为空时线程等待，想到编写一个类似于Java下BlockingQueue的阻塞队列 指定一个日志等级，超过这个等级的日志才会被保存或者打印 通过va_list实现不定参数，使日志记录有sprintf的的使用体验 开始编写 有了上述思路，整体编写就很简单了。 BlockingQueue 偷懒了，这部分直接让DeepSeek写的 为了实现一个多线程安全的阻塞队列，当队列为空时调用front()会阻塞直到其他线程添加元素，我们可以结合互斥锁（std::mutex）和条件变量（std::condition_variable）来同步线程操作。 代码实现 互斥锁（std::mutex） 所有对队列的操作（push、front、pop、empty）都需要先获取锁，确保同一时间只有一个线程能修改队列，避免数据竞争。 条件变量（std::condition_variable） 当调用front()时，如果队列为空，线程会通过cv_.wait()释放锁并阻塞，直到其他线程调用push()添加元素后，通过cv_.notify_one()唤醒一个等待线程。 cv_.wait()需配合std::unique_lock，并在等待时自动释放锁，避免死锁。 使用谓词检查（[this] { return !queue_.empty(); }）防止虚假唤醒。 元素获取与移除 front()返回队首元素的拷贝（而非引用），确保调用者获得数据时队列的锁已释放，避免悬空引用。 pop()需显式调用以移除元素，确保队列状态可控。 #include <queue> // 队列 #include <mutex> // 互斥锁 #include <condition_variable> // 条件变量 template<typename T> class BlockingQueue { public: // 向队列中添加元素 void push(const T& item) { std::lock_guard<std::mutex> lock(mtx_); queue_.push(item); cv_.notify_one(); // 通知一个等待的线程 } // 获取队首元素（阻塞直到队列非空） T front() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); // 阻塞直到队列非空 return queue_.front(); } // 获取队首元素并移除 T take() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); T item = std::move(queue_.front()); // 移动语义避免拷贝 queue_.pop(); return item; } // 移除队首元素（需外部调用，非阻塞） void pop() { std::lock_guard<std::mutex> lock(mtx_); if (!queue_.empty()) { queue_.pop(); } } // 检查队列是否为空 bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } private: mutable std::mutex mtx_; // 互斥锁 std::condition_variable cv_; // 条件变量 std::queue<T> queue_; // 内部队列 }; Log类 Log.h #pragma once #include <iostream> #include <fstream> #include <cstring> #include <thread> #include <chrono> #include <mutex> #include <cstdio> #include <cstdarg> #include <atomic> #include "BlockingQueue.h" enum LogLevel { LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF }; struct LogMsg { short m_LogLevel; std::string m_strTimestamp; std::string m_strLogMsg; }; class Log { private: std::ofstream m_ofLogFile; // 日志文件输出流 std::mutex m_lockFile; // 文件操作互斥锁 std::thread m_threadMain; // 后台日志处理线程 BlockingQueue<LogMsg> m_msgQueue; // 线程安全阻塞队列 short m_levelLog, m_levelPrint; // 文件和控制台日志级别阈值 std::atomic<bool> m_exit_requested{ false }; // 线程退出标志 std::string getTime(); // 获取当前时间戳 std::string level2str(short level, bool character_only); // 级别转字符串 void logThread(); // 后台线程函数 public: Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO); ~Log(); void push(short level, const char* msg, ...); // 添加日志（支持格式化） void set_level(short loglevel, short printlevel); // 设置日志级别 bool open(std::string filename); // 打开日志文件 bool close(); // 关闭日志文件 }; Log.cpp #include "Log.h" std::string Log::getTime() { using sc = std::chrono::system_clock; std::time_t t = sc::to_time_t(sc::now()); char buf[20]; #ifdef _WIN32 std::tm timeinfo; localtime_s(&timeinfo,&t); sprintf_s(buf, "%04d.%02d.%02d-%02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ); #else strftime(buf, 20, "%Y.%m.%d-%H:%M:%S", localtime(&t)); #endif return buf; } std::string Log::level2str(short level, bool character_only) { switch (level) { case LEVEL_VERBOSE: return character_only ? "V" : "Verbose"; case LEVEL_WARN: return character_only ? "W" : "Warning"; case LEVEL_ERROR: return character_only ? "E" : "Error"; case LEVEL_FATAL: return character_only ? "F" : "Fatal"; } return character_only ? "I" : "Info"; } void Log::logThread() { while (true) { LogMsg front = m_msgQueue.take(); // 阻塞直到有消息 // 处理文件写入 if (front.m_LogLevel >= m_levelLog) { std::lock_guard<std::mutex> lock(m_lockFile); // RAII 管理锁 if (m_ofLogFile) { m_ofLogFile << front.m_strTimestamp << ' ' << level2str(front.m_LogLevel, true) << ": " << front.m_strLogMsg << std::endl; } } // 处理控制台打印 if (front.m_LogLevel >= m_levelPrint) { printf("%s %s: %s\n", front.m_strTimestamp.c_str(), level2str(front.m_LogLevel, true).c_str(), front.m_strLogMsg.c_str()); } // 检查退出条件：队列为空且标志为真 if (m_exit_requested.load() && m_msgQueue.empty()) break; } return; } Log::Log(short default_loglevel, short default_printlevel) { set_level(default_loglevel, default_printlevel); m_threadMain = std::thread(&Log::logThread, this); } Log::~Log() { m_exit_requested.store(true); m_msgQueue.push({ LEVEL_INFO, getTime(), "Exit." }); // 唤醒可能阻塞的线程 if (m_threadMain.joinable()) m_threadMain.join(); close(); // 确保文件关闭 } void Log::push(short level, const char* msg, ...) { va_list args; va_start(args, msg); const int len = vsnprintf(nullptr, 0, msg, args); va_end(args); if (len < 0) return; std::vector<char> buf(len + 1); va_start(args, msg); vsnprintf(buf.data(), buf.size(), msg, args); va_end(args); m_msgQueue.push({level,getTime(),buf.data()}); } void Log::set_level(short loglevel, short printlevel) { m_levelLog = loglevel; m_levelPrint = printlevel; } bool Log::open(std::string filename) { m_lockFile.lock(); m_ofLogFile.open(filename.c_str(), std::ios::out); m_lockFile.unlock(); return (bool)m_ofLogFile; } bool Log::close() { m_lockFile.lock(); m_ofLogFile.close(); m_lockFile.unlock(); return false; } 说明 类/结构说明 LogLevel 枚举 定义日志级别：VERBOSE, INFO, WARN, ERROR, FATAL, OFF。 OFF不应被用于记录的日志等级，仅用于当需要关闭日志记录时将阈值设置为该项以实现所有日志都不记录 LogMsg 结构体 封装日志消息： m_LogLevel：日志级别。 m_strTimestamp：时间戳字符串。 m_strLogMsg：日志内容。 成员变量说明 变量 说明 m_ofLogFile 文件输出流，用于写入日志文件。 m_lockFile 互斥锁，保护文件操作。 m_threadMain 后台线程，处理日志消息的消费。 m_msgQueue 阻塞队列，存储待处理的日志消息。 m_levelLog 写入文件的最低日志级别（高于此级别的消息会被记录）。 m_levelPrint 打印到控制台的最低日志级别。 m_exit_requested 原子标志，控制日志线程退出。 函数说明 函数 说明 getTime 获取当前时间戳字符串（跨平台实现）。 level2str 将日志级别转换为字符串（如 LEVEL_INFO → "I" 或 "Info"）。 logThread 后台线程函数：消费队列消息，写入文件或打印。 构造函数 初始化日志级别，启动后台线程。 析构函数 设置退出标志，等待线程结束，确保处理剩余消息。 push 格式化日志消息（支持可变参数）并推入队列。 set_level 动态设置日志级别和打印级别。 open/close 打开/关闭日志文件。 完整代码及测试样例下载： demo.zip
这篇文章是古早时期的博客上迁移过来的（什么赛博秽土转生）， 如有错误欢迎指正 背景 为API配置了多条访问线路，以应对部分地区无法访问导致服务不可用的情况。开始是想到在网站里新建一个文件保存节点信息，发现行不通，联不通的地区根本无法获知其他线路；于是想到用DNS解析记录，用一个TXT解析记录来保存相应的节点数据，以备查询 实战 查询资料 一番Bing下来，发现大部分现有的文章都是使用socket来直接发送查询数据包，获取到的也都是A和CNAME类型的记录，方案不可行。最后，把目光锁定到了MSDN上的DnsQuery函数上，同时找到一篇样例：使用DnsQuery解析主机名 函数分析 从官方文档上得知，函数所需头文件为windns.h，需要包含引入库文件Ws2_32.lib和Dnsapi.lib，函数的参数如下： DNS_STATUS DnsQuery_A( [in] PCSTR pszName, [in] WORD wType, [in] DWORD Options, [in, out, optional] PVOID pExtra, [out, optional] PDNS_RECORD *ppQueryResults, [out, optional] PVOID *pReserved ); 参数解释： pszName是要查询的主机名； wType是查询类型，如A记录，CNAME，TXT等，具体的MSDN官方给出了文档：DNS Constants Options字面意思上理解是查询方式，我直接使用DNS_QUERY_STANDARD，具体也可以查询文档：DNS Constants pExtra和pReserved直接给NULL就行 ppQueryResults传入查询结果的指针，类型是PDNS_RECORD，这个需要传入变量值的引用。 返回值，DNS_STATUS类型，如果查询失败会返回Winerror.h中的相应错误码 使用 明白了使用方法，那就简单了： 我的TXT记录域名为test.iyoroy.cn，查询部分代码如下： PDNS_RECORD pDnsRecord; DNS_STATUS QueryRet = DnsQuery(L"test.iyoroy.cn", DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &pDnsRecord, NULL); if (QueryRet) { MessageBox(GhWnd, L"DNS查询失败！\r\n将使用默认节点", L"Warning", MB_ICONERROR | MB_OK); } std::wstring strQueryRes = *pDnsRecord->Data.TXT.pStringArray;//这个就是查询结果 成功实现功能 后记 TXT记录会自动过滤掉空格、换行，因此我选择记录base64编码，使用时再解码并使用stringstream拆分空格。Base64解码我借鉴了这篇文章：C++进行base64编码和解码，以下是我用宽字节重写的解码函数 {collapse} {collapse-item label="展开代码"} //Function:Base64解密 static const std::wstring base64_chars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" L"abcdefghijklmnopqrstuvwxyz" L"0123456789+/"; static inline bool is_base64(wchar_t c) { return (isalnum(c) || (c == '+') || (c == '/')); } std::wstring base64_decode(std::wstring const& encoded_string) { int in_len = encoded_string.size(); int i = 0; int j = 0; int in_ = 0; wchar_t char_array_4[4], char_array_3[3]; std::wstring ret; while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j < 4; j++) char_array_4[j] = 0; for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } 拆分： wstringstream wDnsRec(base64_decode(*pDnsRecord->Data.TXT.pStringArray)); wstring wNodes[16]; unsigned int nNodes = 0; while (wDnsRec >> wNodes[nNodes]) nNodes++; {/collapse-item} {/collapse} 之后再选择可用节点就行 参考文章： https://learn.microsoft.com/zh-cn/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants https://blog.csdn.net/sky04/article/details/6881649
小米845系列机器类原生调用红外摄像头进行人脸识别
背景 小米845系列的小米8、小米8屏幕指纹版有一颗前置红外摄像头用于面部识别，这样即使在无光的环境下仍可以使用面部解锁。然而如果使用LineageOS的设备树不加修改编译出来的类原生无法使用红外摄像头，只能使用普通的摄像头，而PixelExperience的设备树是可以的，故探究其原因。 原因 CameraID 首先是需要指定人脸识别模块使用红外摄像头。翻阅PixelExperience的相关设备树源码可以发现：在overlay中设置了调用CameraID为5的摄像头用作人脸识别。 https://github.com/PixelExperience-Devices/device_xiaomi_dipper/blob/fourteen/overlay/packages/apps/FaceUnlockService/app/src/main/res/values/config.xml <?xml version="1.0" encoding="utf-8"?> <resources> <integer name="override_front_cam_id">5</integer> <bool name="use_alternative_vendor_impl">true</bool> </resources> PixelExperience使用的是motorola的人脸解锁方案，而现在很多类原生使用的是AOSPA的ParanoidSense，因此上述设置的overlay并不适用。 查阅ParanoidSense源码可以看到：使用了Properties属性ro.face.sense_service.camera_id标识使用的摄像头。 https://github.com/AOSPA/android_packages_apps_ParanoidSense/blob/uvite/src/co/aospa/sense/camera/CameraUtil.kt#L32 val cameraIdProp = SystemProperties.get("ro.face.sense_service.camera_id") 因此理论上只要设置ro.face.sense_service.camera_id属性为5即可使其调用红外人脸摄像头。 vendor.camera.aux.packagelist属性 如果仅仅设置上述CameraID相关属性，那么人脸模块调用时会直接报错。翻阅Frameworks部分的代码可以得知：系统默认隐藏了除主前摄和主后摄之外的其他辅助摄像头（即AUXCamera），而红外摄像头也属于AUXCamera之一。 取LineageOS的Framework代码作为例子： https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L295-L301 /** * Returns the number of physical cameras available on this device. * The return value of this method might change dynamically if the device * supports external cameras and an external camera is connected or * disconnected. * * If there is a * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA * logical multi-camera} in the system, to maintain app backward compatibility, this method will * only expose one camera per facing for all logical camera and physical camera groups. * Use camera2 API to see all cameras. * * @return total number of accessible camera devices, or 0 if there are no * cameras or an error was encountered enumerating them. */ public static int getNumberOfCameras() { int numberOfCameras = _getNumberOfCameras(); if (!shouldExposeAuxCamera() && numberOfCameras > 2) { numberOfCameras = 2; } return numberOfCameras; } 不难看出，即使设备有大于2颗摄像头，若函数shouldExposeAuxCamera()返回为False则会强制指定摄像头数量为2，阻止程序访问CameraID>2的摄像头；再来看看这个判断函数的定义： https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L264-L278 /** * @hide */ public static boolean shouldExposeAuxCamera() { /** * Force to expose only two cameras * if the package name does not falls in this bucket */ String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) return true; List<String> packageList = Arrays.asList( SystemProperties.get("vendor.camera.aux.packagelist", packageName).split(",")); List<String> packageExcludelist = Arrays.asList( SystemProperties.get("vendor.camera.aux.packageexcludelist", "").split(",")); return packageList.contains(packageName) && !packageExcludelist.contains(packageName); } 可以看出，假如包名在属性vendor.camera.aux.packagelist中并且不在vendor.camera.aux.packageexcludelist中就会返回为True，否则False。 那么解决方案就很明了了：将ParanoidSense的包名加到vendor.camera.aux.packagelist属性中即可让其能够调用到红外摄像头。 解决 将ParanoidSense的包名co.aospa.sense加到vendor.camera.aux.packagelist中： https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_sdm845-common/commit/28fe5bc41d49b31b4acb840bf167b70d70a40c61 设置ro.face.sense_service.camera_id属性以指定摄像头： https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_equuleus/commit/67518f130650e4592b5f4c7210248072058d48cc https://github.com/YoriInstitute/AOSPA_device_xiaomi_equuleus/blob/topaz/device.mk#L397-L399 在crDroid魔改的ParanoidSense中，因指定了其位于system_ext（https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1），故将这个属性加到system_ext的prop文件中。
前言 KernelSU出来也有一段时间了，官方只支持GKI内核但是给出了非GKI内核的集成方案。咱使用的小米8屏幕指纹版是基于Android4.9内核的，实测直接使用KernelSU的脚本集成会出问题，故记录下折腾过程 特别感谢：Tomsec大佬提供的4.9修复方案 准备材料 Android Kernel源码，此处使用的是LineageOS/android_kernel_xiaomi_sdm845 能访问GitHub的网络环境 AnyKernel3源码，可参考如下仓库：https://github.com/YoriInstitute/AnyKernel3-equuleus 交叉编译器，可参考如下仓库：https://github.com/YoriInstitute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml#L21-L23 编译服务器 开整 编译环境配置这里就不叙述了，可以放在ROM编译里制作包的时候直接集成在boot.img里也可以单独编译成AnyKernel3可刷入包，这里使用的是AnyKernel3的方案。 下载内核源码、交叉编译器 git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 #内核源码 git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang #Clang git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 #aarch64的GCC git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 #arm的GCC 不同内核需要的交叉编译器可能不同，请根据你的实际情况修改 {alert type="info"} 此处下载内核时没加参数--depth=1是为了后期便于cherry-pick，若你选择手动修改内核源码而不是pick则可以加上此参数，可以显著减少下载时的时间 {/alert} 修改内核源码 参考如下两个commit： https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/7862fd2c14b69a1a346b7c699f8370e2de423eef commit b5853fb7cefdc8a2e160cb73512dc6b51569fa66 Author: OnlyTomInSecond <q2781273965@gmail.com> Date: Wed Apr 5 11:05:12 2023 +0800 kernelsu: allow init exec ksud under nosuid Change-Id: I8aa6e6d3cbee1addd5da9bb48b4c08ce91f9db81 diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c index 4abba0e1674d..693f2ac03352 100644 --- a/security/selinux/hooks.c +++ b/security/selinux/hooks.c @@ -2320,7 +2320,10 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm, { int nnp = (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS); int nosuid = !mnt_may_suid(bprm->file->f_path.mnt); - int rc; + int rc, error; + static u32 ksu_sid; + char *secdata; + u32 seclen; if (!nnp && !nosuid) return 0; /* neither NNP nor nosuid */ @@ -2328,6 +2331,18 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm, if (new_tsec->sid == old_tsec->sid) return 0; /* No change in credentials */ + if(!ksu_sid){ + security_secctx_to_secid("u:r:su:s0", strlen("u:r:su:s0"), &ksu_sid); + } + error = security_secid_to_secctx(old_tsec->sid, &secdata, &seclen); + if (!error) { + rc = strcmp("u:r:init:s0",secdata); + security_release_secctx(secdata, seclen); + if(rc == 0 && new_tsec->sid == ksu_sid){ + return 0; + } + } + /* * The only transitions we permit under NNP or nosuid * are transitions to bounded SIDs, i.e. SIDs that are https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/81d7168f2f01e6aba0932a4787119fbc541eba58 commit 8bfe4d4de25650505798cba0a5a20db4b2ab7dd6 Author: OnlyTomInSecond <q2781273965@gmail.com> Date: Thu Oct 19 17:55:23 2023 +0800 Kernelsu: add path_umount implementation. Change-Id: I3b0273e9857c51cc3215afab0d2cebe0dff38cfb diff --git a/fs/namespace.c b/fs/namespace.c index 21fd423b19cf..219a501337b0 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -1711,6 +1711,38 @@ static inline bool may_mandlock(void) } #endif +static int can_umount(const struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + + if (!may_mount()) + return -EPERM; + if (path->dentry != path->mnt->mnt_root) + return -EINVAL; + if (!check_mnt(mnt)) + return -EINVAL; + if (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */ + return -EINVAL; + if (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN)) + return -EPERM; + return 0; +} + +// caller is responsible for flags being sane +int path_umount(struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + int ret; + + ret = can_umount(path, flags); + if (!ret) + ret = do_umount(mnt, flags); + + /* we mustn't call path_put() as that would clear mnt_expiry_mark */ + dput(path->dentry); + mntput_no_expire(mnt); + return ret; +} /* * Now umount can handle mount points as well as block devices. * This is important for filesystems which use unnamed block devices. 方案来源：Tomsec 启用kprobe支持 打开你的内核的defconfig文件，添加如下三行： CONFIG_KPROBES=y CONFIG_HAVE_KPROBES=y CONFIG_KPROBE_EVENTS=y 集成KernelSU 此步骤和ksu官网教程一样，在内核根目录执行如下指令： curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s v0.9.5 {alert type="warning"} 注：最高支持非GKI内核的KernelSU版本是v0.9.5，请勿使用更高版本的tag否则会编译不通过 {/alert} 编写AnyKernel3脚本 下载下来AnyKernel3，并且按照README修改anykernel.sh，可参考咱的仓库修改： 仓库地址：https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/14/anykernel.sh {collapse} {collapse-item label="展开代码"} commit 3b46e03e0ffdd52bbb3f51b45b0ff649a407d1a5 Author: KaguraiYoRoy <3363384229@qq.com> Date: Sat Sep 9 02:41:54 2023 +0000 anykernel.sh: modify for equuleus diff --git a/anykernel.sh b/anykernel.sh old mode 100755 new mode 100644 index 367d29f..c950ce7 --- a/anykernel.sh +++ b/anykernel.sh @@ -4,23 +4,17 @@ ### AnyKernel setup # global properties properties() { ' -kernel.string=ExampleKernel by osm0sis @ xda-developers +kernel.string=Xiaomi 8 Pro Kernel by Kagura iYoRoy @ iYoRoy Studio do.devicecheck=1 do.modules=0 -do.systemless=1 +do.systemless=0 do.cleanup=1 do.cleanuponabort=0 -device.name1=maguro -device.name2=toro -device.name3=toroplus -device.name4=tuna -device.name5= -supported.versions= +device.name1=equuleus +supported.versions=13 supported.patchlevels= -supported.vendorpatchlevels= '; } # end properties - ### AnyKernel install ## boot files attributes boot_attributes() { @@ -29,7 +23,7 @@ set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; } # end attributes # boot shell variables -block=/dev/block/platform/omap/omap_hsmmc.0/by-name/boot; +block=/dev/block/bootdevice/by-name/boot; is_slot_device=0; ramdisk_compression=auto; patch_vbmeta_flag=auto; @@ -41,32 +35,26 @@ patch_vbmeta_flag=auto; dump_boot; # use split_boot to skip ramdisk unpack, e.g. for devices with init_boot ramdisk # init.rc -backup_file init.rc; -replace_string init.rc "cpuctl cpu,timer_slack" "mount cgroup none /dev/cpuctl cpu" "mount cgroup none /dev/cpuctl cpu,timer_slack"; +#backup_file init.rc; +#replace_string init.rc "cpuctl cpu,timer_slack" "mount cgroup none /dev/cpuctl cpu" "mount cgroup none /dev/cpuctl cpu,timer_slack"; # init.tuna.rc -backup_file init.tuna.rc; -insert_line init.tuna.rc "nodiratime barrier=0" after "mount_all /fstab.tuna" "\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0"; -append_file init.tuna.rc "bootscript" init.tuna; +#backup_file init.tuna.rc; +#insert_line init.tuna.rc "nodiratime barrier=0" after "mount_all /fstab.tuna" "\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0"; +#append_file init.tuna.rc "bootscript" init.tuna; # fstab.tuna -backup_file fstab.tuna; -patch_fstab fstab.tuna /system ext4 options "noatime,barrier=1" "noatime,nodiratime,barrier=0"; -patch_fstab fstab.tuna /cache ext4 options "barrier=1" "barrier=0,nomblk_io_submit"; -patch_fstab fstab.tuna /data ext4 options "data=ordered" "nomblk_io_submit,data=writeback"; -append_file fstab.tuna "usbdisk" fstab; +#backup_file fstab.tuna; +#patch_fstab fstab.tuna /system ext4 options "noatime,barrier=1" "noatime,nodiratime,barrier=0"; +#patch_fstab fstab.tuna /cache ext4 options "barrier=1" "barrier=0,nomblk_io_submit"; +#patch_fstab fstab.tuna /data ext4 options "data=ordered" "nomblk_io_submit,data=writeback"; +#append_file fstab.tuna "usbdisk" fstab; write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_boot ramdisk ## end boot install -## init_boot files attributes -#init_boot_attributes() { -#set_perm_recursive 0 0 755 644 $ramdisk/*; -#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; -#} # end attributes - -# init_boot shell variables +## init_boot shell variables #block=init_boot; #is_slot_device=1; #ramdisk_compression=auto; @@ -98,13 +86,7 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_ ## end vendor_kernel_boot install -## vendor_boot files attributes -#vendor_boot_attributes() { -#set_perm_recursive 0 0 755 644 $ramdisk/*; -#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; -#} # end attributes - -# vendor_boot shell variables +## vendor_boot shell variables #block=vendor_boot; #is_slot_device=1; #ramdisk_compression=auto; @@ -118,4 +100,3 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_ #write_boot; # use flash_boot to skip ramdisk repack, e.g. for dtb on devices with hdr v4 but no vendor_kernel_boot ## end vendor_boot install - {/collapse-item} {/collapse} 编译内核 此处建议参考网上教程，不同内核编译参数设置会有所不同 咱将编译过程打包成了shell脚本，可供参考： {alert type="info"} 请将下方$WORKSPACE替换为你自己的工作目录 {/alert} #!/bin/bash ARCH="arm64" CLANG_DIR="$WORKSPACE/clang/clang-r450784d" #Clang文件夹 CC="$CLANG_DIR/bin/clang" export PATH="$CLANG_DIR/bin:$PATH" OUT_DIR="./out" CLANG_TRIPLE="aarch64-linux-gnu-" CROSS_COMPILE="$WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-" #aarch64GCC CROSS_COMPILE_ARM32="$WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-" #armGCC CC_ADDITION_FLAGS="AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld" ANYKERNEL_DIR="$WORKSPACE/AnyKernel3" #编写好的AnyKernel3文件夹所在位置 THREAD=$(nproc --all) args="-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS" ### 编译内核 ### echo "[+]Args: $args" echo "[+}Generate .config" make equuleus_user_defconfig $args #此处equuleus_user_defconfig为equuleus的内核defconfig文件，请替换成你的内核配置文件名 echo "[+]Begin Build" make $args ### 编译完成 ### ### 制作AnyKernel3 ### cd $WORKSPACE/sdm845/KernelSU KSU_VERSION='v0.9.5' TARGET_KERNEL="Mi8Pro-LineageKernel-KernelSU$KSU_VERSION" #内核文件名 echo "[+]KernelSU Version: $KSU_VERSION" echo "[+]Target file: $TARGET_KERNEL" cd $WORKSPACE/AnyKernel3 cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image . cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz . cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb . OUTPUT_FILE=${TARGET_KERNEL}-$(date +"%y.%m.%d").zip echo "[+]Output: $OUTPUT_FILE" zip -r $OUTPUT_FILE * mv $OUTPUT_FILE $WORKSPACE 刷入 使用twrp之类的recovery即可 使用GitHub Actions自动编译内核 yml编写请参考：https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml {collapse} {collapse-item label="展开代码"} name: Build AnyKernel on: push: schedule: - cron: '0 0 * * 0' jobs: build: runs-on: ubuntu-latest steps: - name: Download run: | echo "Free space:" df -h cd $GITHUB_WORKSPACE echo "[+]Install packages:" sudo apt update sudo apt install zip bc bison build-essential ccache curl flex g++-multilib gcc-multilib git gnupg gperf imagemagick lib32ncurses5-dev lib32readline-dev lib32z1-dev liblz4-tool libncurses5-dev libncurses5 libsdl1.2-dev libssl-dev libwxgtk3.0-gtk3-dev libxml2 libxml2-utils lzop pngcrush rsync schedtool squashfs-tools xsltproc zip zlib1g-dev make unzip python-is-python3 echo "[+]Clone dependencies:" git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 git clone https://github.com/KaguraiYoRoy/AnyKernel3-equuleus AnyKernel3 --depth=1 - name: Setup KernelSU run: | cd $GITHUB_WORKSPACE/sdm845 curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s main - name: Build Kernel run: | cd $GITHUB_WORKSPACE/sdm845 ARCH="arm64" CLANG_DIR="$GITHUB_WORKSPACE/clang/clang-r450784d" CC="$CLANG_DIR/bin/clang" export PATH="$CLANG_DIR/bin:$PATH" OUT_DIR="./out" CLANG_TRIPLE="aarch64-linux-gnu-" CROSS_COMPILE="$GITHUB_WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-" CROSS_COMPILE_ARM32="$GITHUB_WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-" CC_ADDITION_FLAGS="AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld" THREAD=$(nproc --all) ANYKERNEL_DIR="$GITHUB_WORKSPACE/AnyKernel3" args="-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS" echo "[+]Args: $args" echo "[+}Generate .config" make equuleus_user_defconfig $args echo "[+]Begin Build" make $args - name: Compress run: | cd $GITHUB_WORKSPACE/sdm845/KernelSU KSU_VERSION=`expr 10000 + \`git rev-list --count HEAD\` + 200` TARGET_KERNEL="Mi8Pro-LineageKernel-KernelSU$KSU_VERSION" echo "[+]KernelSU Version: $KSU_VERSION" echo "[+]Target file: $TARGET_KERNEL" cd $GITHUB_WORKSPACE/AnyKernel3 cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image . cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz . cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb . OUTPUT_FILE=${TARGET_KERNEL}-$(date +"%y.%m.%d").zip echo "[+]Output: $OUTPUT_FILE" zip -r $OUTPUT_FILE * mv $OUTPUT_FILE $GITHUB_WORKSPACE - name: Release uses: softprops/action-gh-release@v1 with: tag_name: Android13 files: | Mi8Pro*.zip 咱才不会告诉你上面的脚本其实也是这里面摘出来的（逃 {/collapse-item} {/collapse} 头图下载
前言 众所周知，咱是老牌小米8屏幕指纹版钉子户了 咱一直想给这个机子上个erofs（关于erofs是什么请自行搜索 但是经过查阅得知erofs是在5.10被加入到Linux内核中的，也就是说咱4.9得手动集成 准备材料 Android ROM源码：这里使用的是LineageOS 22.0 Android Kernel源码：这里咱用的是LineageOS官方小米845内核：LineageOS/android_kernel_xiaomi_sdm845 设备树：这里使用的是LineageOS官方设备树，包含sdm845-common部分（LineageOS/android_device_xiaomi_sdm845-common）和device-specific部分（LineageOS/android_device_xiaomi_equuleus） 编译服务器 {alert type="info"} 注：因为咱主要是玩编译的比较多，因此这篇主要阐述的是编译ROM过程中的折腾记录 {/alert} 开整 部署Android ROM编译环境就不说了，网上一搜一大片，把ROM源码、设备树、内核源码下载下来放到对应位置 为内核添加erofs支持 此处代码pick from ReallySnow的msm-4.9内核（谢谢ReallySnow大佬的支持！），可参考如下分支： https://github.com/ReallySnow/msm-4.9/tree/erofs https://github.com/YoriInstitute/android_kernel_xiaomi_sdm845/tree/lineage-22.0-erofs pick之前要先处理lz4压缩部分代码的冲突。详细Relation chain可参考： https://review.lineageos.org/c/LineageOS/android_kernel_xiaomi_sdm845/+/412614 咱尝试将改造提交给LineageOS官方但是很显然没被合并x pick完更改后在内核的defconfig文件中开启erofs支持： 打开$KERNEL_SOURCE/arch/arm64/configs/vendor/xiaomi/mi845_defconfig并添加：CONFIG_EROFS_FS=y： commit 7ae5caab2c15982b3d3821c97e2f2e6126ca1281 (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 22:49:07 2024 +0800 arch: arm64: configs: mi845: Enable CONFIG_EROFS_FS Change-Id: Ie387b03bde54700d73ccf19f0baf4db62526cc8f diff --git a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig index 7684be360d98..69ea580fdb11 100644 --- a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig +++ b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig @@ -651,3 +651,4 @@ CONFIG_SOUNDWIRE_WCD_CTRL=y CONFIG_WCD9XXX_CODEC_CORE=y CONFIG_WCD_DSP_GLINK=y CONFIG_WCD_SPI_AC=y +CONFIG_EROFS_FS=y \ No newline at end of file 修改分区文件系统和fstab 打开设备树中板级配置文件把要改为erofs的分区的文件系统改为erofs，示例中的配置文件在$WORKING_DIR/device/xiaomi/sdm845-common/BoardConfigCommon.mk： commit 0fce86fb40ca637a8563bef6192afaea1658c881 (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 23:37:50 2024 +0800 sdm845-common: Switch to erofs Change-Id: I9a8d6de4dcb79cd6bc177451ce3cbc4682748501 diff --git a/BoardConfigCommon.mk b/BoardConfigCommon.mk index cf73d39..700592f 100644 --- a/BoardConfigCommon.mk +++ b/BoardConfigCommon.mk @@ -78,11 +78,11 @@ BOARD_CACHEIMAGE_PARTITION_SIZE := 268435456 BOARD_USERDATAIMAGE_PARTITION_SIZE := 57453555712 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4 -BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := ext4 +BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE := ext4 BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4 BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE := ext4 -BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := ext4 +BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_FLASH_BLOCK_SIZE := 262144 # (BOARD_KERNEL_PAGESIZE * 64) 修改机器fstab文件，此处在$WORKING_DIR/device/xiaomi/equuleus/rootdir/fstab.qcom，将挂载文件系统改为erofs，挂载参数仅保留一个ro即可： commit d66bfc8fe722f7368ef56835d35fb88b904e10fb (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 23:40:08 2024 +0800 equuleus: Update fstab for erofs Change-Id: I814eab1dd77cf2c184ebd398cc4914785bc37ad6 diff --git a/rootdir/etc/fstab.qcom b/rootdir/etc/fstab.qcom index 7aca90a..685ec84 100644 --- a/rootdir/etc/fstab.qcom +++ b/rootdir/etc/fstab.qcom @@ -6,8 +6,8 @@ system /system ext4 ro,barrier=1,discard wait,logical,first_stage_mount system_ext /system_ext ext4 ro,barrier=1,discard wait,logical,first_stage_mount product /product ext4 ro,barrier=1,discard wait,logical,first_stage_mount -vendor /vendor ext4 ro,barrier=1,discard wait,logical,first_stage_mount -odm /odm ext4 ro,barrier=1,discard wait,logical,first_stage_mount +vendor /vendor erofs ro wait,logical,first_stage_mount +odm /odm erofs ro wait,logical,first_stage_mount /dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=0,noauto_da_alloc latemount,wait,check,fileencryption=ice,quota /dev/block/bootdevice/by-name/modem /vendor/firmware_mnt vfat ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:object_r:firmware_file:s0 wait /dev/block/bootdevice/by-name/dsp /vendor/dsp ext4 ro,nosuid,nodev,barrier=1 wait 最后编译并刷入，如果不出意外的话就能启动进系统了。 特别注意 LineageOS官方对我这款机型做了动态分区改造，上述设备树的修改可能并不适用于你的机型 erofs为只读文件系统，如果后期要刷Gapps，请勿改造system、system_ext、product这三个分区 常见问题 开机进入fastboot：检查fstab是否修改正确 头图下载