Updated on 2026-08-14
This commit is contained in:
parent
8a83664e9a
commit
728187e890
11 changed files with 231 additions and 139 deletions
36
.gitattributes
vendored
Normal file
36
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# Normalize all text files to LF in the repository and the working tree on
|
||||||
|
# every OS, regardless of the contributor's core.autocrlf setting.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Windows script files must stay CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Unix scripts and the Gradle wrapper must stay LF.
|
||||||
|
*.sh text eol=lf
|
||||||
|
gradlew text eol=lf
|
||||||
|
|
||||||
|
# Binary files must be left untouched (never EOL-normalized or diffed as text).
|
||||||
|
# Images
|
||||||
|
*.png binary
|
||||||
|
*.webp binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
# Fonts
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
# JVM / Android artifacts
|
||||||
|
*.jar binary
|
||||||
|
*.aar binary
|
||||||
|
*.so binary
|
||||||
|
# Signing keys & keystores
|
||||||
|
*.jks binary
|
||||||
|
*.keystore binary
|
||||||
|
# Archives & databases
|
||||||
|
*.zip binary
|
||||||
|
*.db binary
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -48,4 +48,5 @@ find-latest-release-branch.output
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
/.claude/worktrees/
|
/.claude/worktrees/
|
||||||
|
/.claude/settings.local.json
|
||||||
CLAUDE.local.md
|
CLAUDE.local.md
|
||||||
|
|
|
||||||
5
.worktreeinclude
Normal file
5
.worktreeinclude
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Gitignored files copied into new Claude Code worktrees.
|
||||||
|
# Syntax: .gitignore patterns. Only gitignored matches are copied.
|
||||||
|
.claude/settings.local.json
|
||||||
|
app/google-services.json
|
||||||
|
local.properties
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<bool name="largeHeap">true</bool>
|
<bool name="largeHeap">true</bool>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Declare custom theme attributes that allow changing which styles are
|
<!-- Declare custom theme attributes that allow changing which styles are
|
||||||
used for button bars depending on the API level.
|
used for button bars depending on the API level.
|
||||||
?android:attr/buttonBarStyle is new as of API 11 so this is
|
?android:attr/buttonBarStyle is new as of API 11 so this is
|
||||||
necessary to support previous API levels. -->
|
necessary to support previous API levels. -->
|
||||||
<declare-styleable name="ButtonBarContainerTheme">
|
<declare-styleable name="ButtonBarContainerTheme">
|
||||||
<attr name="metaButtonBarStyle" format="reference" />
|
<attr name="metaButtonBarStyle" format="reference" />
|
||||||
<attr name="metaButtonBarButtonStyle" format="reference" />
|
<attr name="metaButtonBarButtonStyle" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<bool name="largeHeap">true</bool>
|
<bool name="largeHeap">true</bool>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -61,15 +62,15 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
|
||||||
|
|
||||||
require(actual == expected) {
|
require(actual == expected) {
|
||||||
"Design tokens are out of date!\n" +
|
"Design tokens are out of date!\n" +
|
||||||
" ds-tokens hash: $actual\n" +
|
" computed from ds-tokens: $actual\n" +
|
||||||
" generated hash: $expected\n" +
|
" committed .tokens-hash: $expected\n" +
|
||||||
"Run the token generator: cd core/ui/token-gen && npm run build"
|
"Run the token generator: cd core/ui/token-gen && npm run build"
|
||||||
}
|
}
|
||||||
|
|
||||||
stampFile.get().asFile.writeText(actual)
|
stampFile.get().asFile.writeText(actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hashTreeHex(root: java.io.File, extension: String): String {
|
private fun hashTreeHex(root: File, extension: String): String {
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
val files = root.walkTopDown()
|
val files = root.walkTopDown()
|
||||||
.filter { it.isFile && it.extension == extension }
|
.filter { it.isFile && it.extension == extension }
|
||||||
|
|
@ -79,12 +80,27 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray())
|
digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray())
|
||||||
digest.update(nul)
|
digest.update(nul)
|
||||||
digest.update(file.readBytes())
|
digest.update(file.readBytes().stripCr())
|
||||||
digest.update(nul)
|
digest.update(nul)
|
||||||
}
|
}
|
||||||
return digest.digest()
|
return digest.digest()
|
||||||
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
|
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips CR (0x0D) bytes so the token hash ignores CRLF vs LF line endings. The ds-tokens
|
||||||
|
* submodule is not covered by this repo's .gitattributes, so its .json/.svg sources may be
|
||||||
|
* checked out with CRLF on some platforms; without this the verification is non-deterministic.
|
||||||
|
* Removes lone CRs too — safe for UTF-8 sources, where 0x0D never appears inside a
|
||||||
|
* multi-byte sequence. Must stay byte-for-byte identical to stripCr() in token-gen/hash-util.mjs.
|
||||||
|
*/
|
||||||
|
private fun ByteArray.stripCr(): ByteArray {
|
||||||
|
val cr: Byte = 0x0D
|
||||||
|
if (none { it == cr }) return this
|
||||||
|
val out = ByteArrayOutputStream(size)
|
||||||
|
for (b in this) if (b != cr) out.write(b.toInt())
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
android {
|
android {
|
||||||
namespace = "com.tangem.core.ui"
|
namespace = "com.tangem.core.ui"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { stripCr, compareCodeUnits } from './hash-util.mjs';
|
||||||
|
|
||||||
// ── Paths ──────────────────────────────────────────────────────────────────────
|
// ── Paths ──────────────────────────────────────────────────────────────────────
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -252,16 +253,17 @@ object Icons
|
||||||
|
|
||||||
function computeIconsHash() {
|
function computeIconsHash() {
|
||||||
const files = [...walkSvgs(iconsDir)];
|
const files = [...walkSvgs(iconsDir)];
|
||||||
files.sort((a, b) => {
|
// Code-unit order (not localeCompare) to match the Kotlin verifier's invariantSeparatorsPath
|
||||||
const ra = path.relative(iconsDir, a).split(path.sep).join('/');
|
// sort deterministically across locales/ICU versions — see hash-util.mjs.
|
||||||
const rb = path.relative(iconsDir, b).split(path.sep).join('/');
|
files.sort((a, b) => compareCodeUnits(
|
||||||
return ra.localeCompare(rb);
|
path.relative(iconsDir, a).split(path.sep).join('/'),
|
||||||
});
|
path.relative(iconsDir, b).split(path.sep).join('/'),
|
||||||
|
));
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
hash.update(path.relative(iconsDir, file).split(path.sep).join('/'));
|
hash.update(path.relative(iconsDir, file).split(path.sep).join('/'));
|
||||||
hash.update('\0');
|
hash.update('\0');
|
||||||
hash.update(fs.readFileSync(file));
|
hash.update(stripCr(fs.readFileSync(file)));
|
||||||
hash.update('\0');
|
hash.update('\0');
|
||||||
}
|
}
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { buildIcons } from './build-icons.mjs';
|
import { buildIcons } from './build-icons.mjs';
|
||||||
|
import { stripCr, compareCodeUnits } from './hash-util.mjs';
|
||||||
|
|
||||||
// ── Paths ──────────────────────────────────────────────────────────────────────
|
// ── Paths ──────────────────────────────────────────────────────────────────────
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -792,18 +793,18 @@ function computeTokensHash() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk(tokensDir);
|
walk(tokensDir);
|
||||||
// Sort by relative path with forward slashes to match Gradle's invariantSeparatorsPath sorting
|
// Sort by forward-slash relative path in UTF-16 code-unit order to match the Kotlin verifier's
|
||||||
files.sort((a, b) => {
|
// sortedBy { …invariantSeparatorsPath } (String.compareTo) deterministically — see hash-util.mjs.
|
||||||
const ra = path.relative(tokensDir, a).split(path.sep).join('/');
|
files.sort((a, b) => compareCodeUnits(
|
||||||
const rb = path.relative(tokensDir, b).split(path.sep).join('/');
|
path.relative(tokensDir, a).split(path.sep).join('/'),
|
||||||
return ra.localeCompare(rb);
|
path.relative(tokensDir, b).split(path.sep).join('/'),
|
||||||
});
|
));
|
||||||
|
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
hash.update(path.relative(tokensDir, file).split(path.sep).join('/'));
|
hash.update(path.relative(tokensDir, file).split(path.sep).join('/'));
|
||||||
hash.update('\0');
|
hash.update('\0');
|
||||||
hash.update(fs.readFileSync(file));
|
hash.update(stripCr(fs.readFileSync(file)));
|
||||||
hash.update('\0');
|
hash.update('\0');
|
||||||
}
|
}
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
|
|
|
||||||
31
core/ui/token-gen/hash-util.mjs
Normal file
31
core/ui/token-gen/hash-util.mjs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Shared hashing helpers for the design-token generators (build-tokens.mjs / build-icons.mjs).
|
||||||
|
// Kept in one place because the exact byte stream AND file ordering produced here must stay
|
||||||
|
// identical to the Kotlin verifier in core/ui/build.gradle.kts (VerifyDesignTokensTask):
|
||||||
|
// any drift silently breaks the token hash gate.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip every CR (0x0D) byte so the hash ignores CRLF vs LF line endings. The ds-tokens submodule
|
||||||
|
* is not covered by the parent repo's .gitattributes, so its .json/.svg sources may be checked out
|
||||||
|
* with CRLF on some platforms. Lone CRs are dropped too — safe here because these sources are UTF-8
|
||||||
|
* (0x0D never appears inside a multi-byte sequence) and carry no bare CR.
|
||||||
|
* Must stay byte-for-byte identical to ByteArray.stripCr() in core/ui/build.gradle.kts.
|
||||||
|
*/
|
||||||
|
export function stripCr(buf) {
|
||||||
|
const out = Buffer.allocUnsafe(buf.length);
|
||||||
|
let n = 0;
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
if (buf[i] !== 0x0d) out[n++] = buf[i];
|
||||||
|
}
|
||||||
|
return out.subarray(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two forward-slash relative paths by UTF-16 code unit — identical to Kotlin's
|
||||||
|
* String.compareTo used by `sortedBy { …invariantSeparatorsPath }` in the verifier.
|
||||||
|
* Do NOT use String.prototype.localeCompare for file ordering: it is locale/ICU-version dependent
|
||||||
|
* (differs across machines) and case-insensitive at the primary level (disagrees with the Kotlin
|
||||||
|
* code-unit sort) — either would reorder files and change the hash.
|
||||||
|
*/
|
||||||
|
export function compareCodeUnits(a, b) {
|
||||||
|
return a < b ? -1 : a > b ? 1 : 0;
|
||||||
|
}
|
||||||
200
gradlew.bat
vendored
200
gradlew.bat
vendored
|
|
@ -1,100 +1,100 @@
|
||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
:init
|
||||||
@rem Get command-line arguments, handling Windows variants
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
:win9xME_args
|
:win9xME_args
|
||||||
@rem Slurp the command line arguments.
|
@rem Slurp the command line arguments.
|
||||||
set CMD_LINE_ARGS=
|
set CMD_LINE_ARGS=
|
||||||
set _SKIP=2
|
set _SKIP=2
|
||||||
|
|
||||||
:win9xME_args_slurp
|
:win9xME_args_slurp
|
||||||
if "x%~1" == "x" goto execute
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue