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,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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue