diff mbox series

[RFC] cargo_common.bbclass: use source replacement instead of dependency patching

Message ID 20251003213000.2256939-1-skandigraun@gmail.com
State New
Headers show
Series [RFC] cargo_common.bbclass: use source replacement instead of dependency patching | expand

Commit Message

Gyorgy Sarvari Oct. 3, 2025, 9:30 p.m. UTC
Cargo.toml files usually contain a list of dependencies in one of two forms:
either a crate name that can be fetched from some registry (like crates.io), or
as a source crate, which is most often fetched from a git repository.

Normally cargo handles fetching the crates from both the registry and from git,
however with Yocto this task is taken over by Bitbake.

After fetching these crates, they are made available to cargo by adding the location
to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
that can be found in the SRC_URI is added as one source crate.

This works most of the time, as long as the repository really contains one crate only.

However in case the repository is a cargo workspace, it contains multiple crates in
different subfolders, and in order to allow cargo to process them, they need to be
listed separately. This is not happening with the current implementation of cargo_common.

This change introduces the following:
- instead of patching the dependencies, use source replacement (the primary motivation for
  this was that maturin seems to ignore source crate patches from config.toml)
- the above also allows to keep the original Cargo.lock untouched (the original implementation
  deleted git repository lines from it)
- it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
  the separate crate folders are copied into this folder, and it is used as the central
  vendoring folder. This is needed for source replacements: the folder that is used for
  vendoring needs to contain the crates separately, one crate in one folder. Each folder
  has the name of the crate that it contains. Workspaces are not included here (unless the
  given manifest is a workspace AND a package at once)
- previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
  to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
  Having destsuffix is still mandatory though.

The change does not handle nested workspaces, only the top level Cargo.toml is processed.

Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
Cc: Tom Geelen <t.f.g.geelen@gmail.com>

---
 meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
 1 file changed, 108 insertions(+), 50 deletions(-)

Comments

Mathieu Dubois-Briand Oct. 5, 2025, 1:23 p.m. UTC | #1
On Fri Oct 3, 2025 at 11:30 PM CEST, Gyorgy Sarvari via lists.openembedded.org wrote:
> Cargo.toml files usually contain a list of dependencies in one of two forms:
> either a crate name that can be fetched from some registry (like crates.io), or
> as a source crate, which is most often fetched from a git repository.
>
> Normally cargo handles fetching the crates from both the registry and from git,
> however with Yocto this task is taken over by Bitbake.
>
> After fetching these crates, they are made available to cargo by adding the location
> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
> that can be found in the SRC_URI is added as one source crate.
>
> This works most of the time, as long as the repository really contains one crate only.
>
> However in case the repository is a cargo workspace, it contains multiple crates in
> different subfolders, and in order to allow cargo to process them, they need to be
> listed separately. This is not happening with the current implementation of cargo_common.
>
> This change introduces the following:
> - instead of patching the dependencies, use source replacement (the primary motivation for
>   this was that maturin seems to ignore source crate patches from config.toml)
> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>   deleted git repository lines from it)
> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>   the separate crate folders are copied into this folder, and it is used as the central
>   vendoring folder. This is needed for source replacements: the folder that is used for
>   vendoring needs to contain the crates separately, one crate in one folder. Each folder
>   has the name of the crate that it contains. Workspaces are not included here (unless the
>   given manifest is a workspace AND a package at once)
> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>   to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>   Having destsuffix is still mandatory though.
>
> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>
> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>
> ---

Hi Gyorgy,

I know this is just an RFC, but I still took it for a run on the
autobuilder. It looks like id does break rust build:

ERROR: rust-native-1.90.0-r0 do_configure: Error executing a python function in exec_func_python() autogenerated:

The stack trace of python calls that resulted in this exception/failure was:
File: 'exec_func_python() autogenerated', lineno: 2, function: <module>
     0001:
 *** 0002:cargo_common_do_patch_paths(d)
     0003:
File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 182, function: cargo_common_do_patch_paths
     0178:    lockfile = d.getVar("CARGO_LOCK_PATH")
     0179:    if not os.path.exists(lockfile):
     0180:        bb.fatal(f"{lockfile} file doesn't exist")
     0181:
 *** 0182:    lockfile = load_toml_file(lockfile)
     0183:
     0184:    # key is the repo url, value is a boolean, which is used later
     0185:    # to indicate if there is a matching repository in SRC_URI also
     0186:    lockfile_git_repos = {}
File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 137, function: load_toml_file
     0133:        cargo_toml_path = os.path.join(path, 'Cargo.toml')
     0134:        return os.path.exists(cargo_toml_path)
     0135:
     0136:    def load_toml_file(toml_path):
 *** 0137:        import tomllib
     0138:        with open(toml_path, 'rb') as f:
     0139:            toml = tomllib.load(f)
     0140:        return toml
     0141:
Exception: ModuleNotFoundError: No module named 'tomllib'

https://autobuilder.yoctoproject.org/valkyrie/#/builders/4/builds/2528
https://autobuilder.yoctoproject.org/valkyrie/#/builders/9/builds/2505
https://autobuilder.yoctoproject.org/valkyrie/#/builders/6/builds/2543
https://autobuilder.yoctoproject.org/valkyrie/#/builders/20/builds/2497

Thanks,
Mathieu
Gyorgy Sarvari Oct. 5, 2025, 1:31 p.m. UTC | #2
On 10/5/25 15:23, Mathieu Dubois-Briand wrote:
> On Fri Oct 3, 2025 at 11:30 PM CEST, Gyorgy Sarvari via lists.openembedded.org wrote:
>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>> either a crate name that can be fetched from some registry (like crates.io), or
>> as a source crate, which is most often fetched from a git repository.
>>
>> Normally cargo handles fetching the crates from both the registry and from git,
>> however with Yocto this task is taken over by Bitbake.
>>
>> After fetching these crates, they are made available to cargo by adding the location
>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>> that can be found in the SRC_URI is added as one source crate.
>>
>> This works most of the time, as long as the repository really contains one crate only.
>>
>> However in case the repository is a cargo workspace, it contains multiple crates in
>> different subfolders, and in order to allow cargo to process them, they need to be
>> listed separately. This is not happening with the current implementation of cargo_common.
>>
>> This change introduces the following:
>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>   this was that maturin seems to ignore source crate patches from config.toml)
>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>   deleted git repository lines from it)
>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>   the separate crate folders are copied into this folder, and it is used as the central
>>   vendoring folder. This is needed for source replacements: the folder that is used for
>>   vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>   has the name of the crate that it contains. Workspaces are not included here (unless the
>>   given manifest is a workspace AND a package at once)
>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>   to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>   Having destsuffix is still mandatory though.
>>
>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>
>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>
>> ---
> Hi Gyorgy,
>
> I know this is just an RFC, but I still took it for a run on the
> autobuilder. It looks like id does break rust build:
>
> ERROR: rust-native-1.90.0-r0 do_configure: Error executing a python function in exec_func_python() autogenerated:
>
> The stack trace of python calls that resulted in this exception/failure was:
> File: 'exec_func_python() autogenerated', lineno: 2, function: <module>
>      0001:
>  *** 0002:cargo_common_do_patch_paths(d)
>      0003:
> File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 182, function: cargo_common_do_patch_paths
>      0178:    lockfile = d.getVar("CARGO_LOCK_PATH")
>      0179:    if not os.path.exists(lockfile):
>      0180:        bb.fatal(f"{lockfile} file doesn't exist")
>      0181:
>  *** 0182:    lockfile = load_toml_file(lockfile)
>      0183:
>      0184:    # key is the repo url, value is a boolean, which is used later
>      0185:    # to indicate if there is a matching repository in SRC_URI also
>      0186:    lockfile_git_repos = {}
> File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 137, function: load_toml_file
>      0133:        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>      0134:        return os.path.exists(cargo_toml_path)
>      0135:
>      0136:    def load_toml_file(toml_path):
>  *** 0137:        import tomllib
>      0138:        with open(toml_path, 'rb') as f:
>      0139:            toml = tomllib.load(f)
>      0140:        return toml
>      0141:
> Exception: ModuleNotFoundError: No module named 'tomllib'

Thanks a lot for testing.
Looks like I got to target older Python versions than 3.11... is 3.9 the
oldest that is still supported?

> https://autobuilder.yoctoproject.org/valkyrie/#/builders/4/builds/2528
> https://autobuilder.yoctoproject.org/valkyrie/#/builders/9/builds/2505
> https://autobuilder.yoctoproject.org/valkyrie/#/builders/6/builds/2543
> https://autobuilder.yoctoproject.org/valkyrie/#/builders/20/builds/2497
>
> Thanks,
> Mathieu
>
Peter Kjellerstedt Oct. 5, 2025, 7:48 p.m. UTC | #3
> -----Original Message-----
> From: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org> On Behalf Of Gyorgy Sarvari via lists.openembedded.org
> Sent: den 5 oktober 2025 15:31
> To: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>; openembedded-core@lists.openembedded.org
> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
> Subject: Re: [OE-core] [RFC PATCH] cargo_common.bbclass: use source replacement instead of dependency patching
> 
> On 10/5/25 15:23, Mathieu Dubois-Briand wrote:
> > On Fri Oct 3, 2025 at 11:30 PM CEST, Gyorgy Sarvari via lists.openembedded.org wrote:
> >> Cargo.toml files usually contain a list of dependencies in one of two forms:
> >> either a crate name that can be fetched from some registry (like crates.io), or
> >> as a source crate, which is most often fetched from a git repository.
> >>
> >> Normally cargo handles fetching the crates from both the registry and from git,
> >> however with Yocto this task is taken over by Bitbake.
> >>
> >> After fetching these crates, they are made available to cargo by adding the location
> >> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
> >> that can be found in the SRC_URI is added as one source crate.
> >>
> >> This works most of the time, as long as the repository really contains one crate only.
> >>
> >> However in case the repository is a cargo workspace, it contains multiple crates in
> >> different subfolders, and in order to allow cargo to process them, they need to be
> >> listed separately. This is not happening with the current implementation of cargo_common.
> >>
> >> This change introduces the following:
> >> - instead of patching the dependencies, use source replacement (the primary motivation for
> >>   this was that maturin seems to ignore source crate patches from config.toml)
> >> - the above also allows to keep the original Cargo.lock untouched (the original implementation
> >>   deleted git repository lines from it)
> >> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
> >>   the separate crate folders are copied into this folder, and it is used as the central
> >>   vendoring folder. This is needed for source replacements: the folder that is used for
> >>   vendoring needs to contain the crates separately, one crate in one folder. Each folder
> >>   has the name of the crate that it contains. Workspaces are not included here (unless the
> >>   given manifest is a workspace AND a package at once)
> >> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
> >>   to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
> >>   Having destsuffix is still mandatory though.
> >>
> >> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
> >>
> >> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
> >> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
> >>
> >> ---
> > Hi Gyorgy,
> >
> > I know this is just an RFC, but I still took it for a run on the
> > autobuilder. It looks like id does break rust build:
> >
> > ERROR: rust-native-1.90.0-r0 do_configure: Error executing a python function in exec_func_python() autogenerated:
> >
> > The stack trace of python calls that resulted in this exception/failure was:
> > File: 'exec_func_python() autogenerated', lineno: 2, function: <module>
> >      0001:
> >  *** 0002:cargo_common_do_patch_paths(d)
> >      0003:
> > File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 182, function: cargo_common_do_patch_paths
> >      0178:    lockfile = d.getVar("CARGO_LOCK_PATH")
> >      0179:    if not os.path.exists(lockfile):
> >      0180:        bb.fatal(f"{lockfile} file doesn't exist")
> >      0181:
> >  *** 0182:    lockfile = load_toml_file(lockfile)
> >      0183:
> >      0184:    # key is the repo url, value is a boolean, which is used later
> >      0185:    # to indicate if there is a matching repository in SRC_URI also
> >      0186:    lockfile_git_repos = {}
> > File: '/srv/pokybuild/yocto-worker/genericx86-64/build/meta/classes-recipe/cargo_common.bbclass', lineno: 137, function: load_toml_file
> >      0133:        cargo_toml_path = os.path.join(path, 'Cargo.toml')
> >      0134:        return os.path.exists(cargo_toml_path)
> >      0135:
> >      0136:    def load_toml_file(toml_path):
> >  *** 0137:        import tomllib
> >      0138:        with open(toml_path, 'rb') as f:
> >      0139:            toml = tomllib.load(f)
> >      0140:        return toml
> >      0141:
> > Exception: ModuleNotFoundError: No module named 'tomllib'
> 
> Thanks a lot for testing.
> Looks like I got to target older Python versions than 3.11... is 3.9 the
> oldest that is still supported?

Yes (see sanity.bbclass).

> 
> > https://autobuilder.yoctoproject.org/valkyrie/#/builders/4/builds/2528
> > https://autobuilder.yoctoproject.org/valkyrie/#/builders/9/builds/2505
> > https://autobuilder.yoctoproject.org/valkyrie/#/builders/6/builds/2543
> > https://autobuilder.yoctoproject.org/valkyrie/#/builders/20/builds/2497
> >
> > Thanks,
> > Mathieu
> >

//Peter
Stefan Herbrechtsmeier Oct. 7, 2025, 2:59 p.m. UTC | #4
Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
> Cargo.toml files usually contain a list of dependencies in one of two forms:
> either a crate name that can be fetched from some registry (like crates.io), or
> as a source crate, which is most often fetched from a git repository.
>
> Normally cargo handles fetching the crates from both the registry and from git,
> however with Yocto this task is taken over by Bitbake.
>
> After fetching these crates, they are made available to cargo by adding the location
> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
> that can be found in the SRC_URI is added as one source crate.
>
> This works most of the time, as long as the repository really contains one crate only.
>
> However in case the repository is a cargo workspace, it contains multiple crates in
> different subfolders, and in order to allow cargo to process them, they need to be
> listed separately. This is not happening with the current implementation of cargo_common.
>
> This change introduces the following:
> - instead of patching the dependencies, use source replacement (the primary motivation for
>    this was that maturin seems to ignore source crate patches from config.toml)
> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>    deleted git repository lines from it)
> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>    the separate crate folders are copied into this folder, and it is used as the central
>    vendoring folder. This is needed for source replacements: the folder that is used for
>    vendoring needs to contain the crates separately, one crate in one folder. Each folder
>    has the name of the crate that it contains. Workspaces are not included here (unless the
>    given manifest is a workspace AND a package at once)
> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>    to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>    Having destsuffix is still mandatory though.
>
> The change does not handle nested workspaces, only the top level Cargo.toml is processed.

I use a similar approach for my Cargo.lock fetcher. In my case the code 
finds the crate on the fly inside the a git repository because the 
Cargo.lock doesn't contain the subpath.


> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>
> ---
>   meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>   1 file changed, 108 insertions(+), 50 deletions(-)
>
> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
> index c9eb2d09a5..79c1351298 100644
> --- a/meta/classes-recipe/cargo_common.bbclass
> +++ b/meta/classes-recipe/cargo_common.bbclass
> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>   python cargo_common_do_patch_paths() {
>       import shutil
>   
> +    def is_rust_crate_folder(path):
> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
> +        return os.path.exists(cargo_toml_path)
> +
> +    def load_toml_file(toml_path):
> +        import tomllib
> +        with open(toml_path, 'rb') as f:
> +            toml = tomllib.load(f)
> +        return toml
> +
> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
> +        for lf_repo in lockfile_repos.keys():
> +            if repo in lf_repo and lf_repo.endswith(revision):

Does this works if the URL contains a "rev" query parameter? This 
happens if the same git repository is used with different revisions.

> +                lockfile_repos[lf_repo] = True
> +                return lf_repo.split("#")[0]
> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
> +
> +    def create_cargo_checksum(folder_path):
> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
> +        if os.path.exists(checksum_path):
> +            return
> +
> +        import hashlib, json
> +
> +        checksum = {'files': {}}
> +        for root, _, files in os.walk(folder_path):
> +            for f in files:
> +                full_path = os.path.join(root, f)
> +                relative_path = os.path.relpath(full_path, folder_path)
> +                if relative_path.startswith(".git/"):
> +                    continue
> +                with open(full_path, 'rb') as f2:
> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
> +                checksum["files"][relative_path] = file_sha

Do we really need the calculation of the checksum?

> +
> +        with open(checksum_path, 'w') as f:
> +            json.dump(checksum, f)
> +
>       cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>       if not os.path.exists(cargo_config):
>           return
> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>       if len(src_uri) == 0:
>           return
>   
> -    patches = dict()
> +    lockfile = d.getVar("CARGO_LOCK_PATH")
> +    if not os.path.exists(lockfile):
> +        bb.fatal(f"{lockfile} file doesn't exist")
> +
> +    lockfile = load_toml_file(lockfile)
> +
> +    # key is the repo url, value is a boolean, which is used later
> +    # to indicate if there is a matching repository in SRC_URI also
> +    lockfile_git_repos = {}
> +    for p in lockfile['package']:
> +        if 'source' in p and p['source'].startswith('git+'):
> +            lockfile_git_repos[p['source']] = False
> +
> +    sources = dict()
>       workdir = d.getVar('UNPACKDIR')
>       fetcher = bb.fetch2.Fetch(src_uri, d)
> +
> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
> +
> +    os.makedirs(vendor_folder)
> +
>       for url in fetcher.urls:
>           ud = fetcher.ud[url]
> -        if ud.type == 'git' or ud.type == 'gitsm':
> -            name = ud.parm.get('name')
> -            destsuffix = ud.parm.get('destsuffix')
> -            if name is not None and destsuffix is not None:
> -                if ud.user:
> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
> -                else:
> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
> -                patches.setdefault(repo, []).append(path)
> +        if ud.type != 'git' and ud.type != 'gitsm':
> +            continue
>   
> -    with open(cargo_config, "a+") as config:
> -        for k, v in patches.items():
> -            print('\n[patch."%s"]' % k, file=config)
> -            for name in v:
> -                print(name, file=config)
> +        destsuffix = ud.parm.get('destsuffix')
> +        crate_folder = os.path.join(workdir, destsuffix)
>   
> -    if not patches:
> -        return
> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
> +            continue
>   
> -    # Cargo.lock file is needed for to be sure that artifacts
> -    # downloaded by the fetch steps are those expected by the
> -    # project and that the possible patches are correctly applied.
> -    # Moreover since we do not want any modification
> -    # of this file (for reproducibility purpose), we prevent it by
> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
> -    # here is better than letting cargo tell (in case the file is missing)
> -    # "Cargo.lock should be modified but --frozen was given"
> +        if ud.user:
> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
> +        else:
> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>   
> -    lockfile = d.getVar("CARGO_LOCK_PATH")
> -    if not os.path.exists(lockfile):
> -        bb.fatal(f"{lockfile} file doesn't exist")
> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
> +
> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
> +        cargo_toml = load_toml_file(cargo_toml_path)
> +
> +        if 'workspace' in cargo_toml:
> +            members = cargo_toml['workspace']['members']
> +            for member in members:
> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
> +                member_crate_name = member_cargo_toml['package']['name']
> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
> +
> +        if 'package' in cargo_toml:
> +            crate_folder = os.path.join(workdir, destsuffix)
> +            crate_name = cargo_toml['package']['name']
> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
> +
> +    for d in os.scandir(vendor_folder):
> +        if d.is_dir():
> +            create_cargo_checksum(d.path)
> +
> +
> +    with open(cargo_config, "a+") as config:
> +        print('\n[source."yocto-vendored-sources"]', file=config)
> +        print('directory = "%s"' % vendor_folder, file=config)
> +
> +        for destsuffix, (repo, revision, repo_path) in sources.items():
> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
> +            print('\n[source."%s"]' % lockfile_repo, file=config)
> +            print('git = "%s"' % repo, file=config)
> +            print('rev = "%s"' % revision, file=config)
> +            print('replace-with = "yocto-vendored-sources"', file=config)
> +
> +    # check if there are any git repos in the lock file that were not visited
> +    # in the previous loop, when the source replacement was created, and warn about it
> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
> +        if not found_in_src_uri:
> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>   
> -    # There are patched files and so Cargo.lock should be modified but we use
> -    # --frozen so let's handle that modifications here.
> -    #
> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
> -    # patched packages:
> -    #  cargo update --offline -p package_1 -p package_2
> -    # But this is not possible since it requires that cargo local git db
> -    # to be populated and this is not the case as we fetch git repo ourself.
> -
> -    lockfile_orig = lockfile + ".orig"
> -    if not os.path.exists(lockfile_orig):
> -        shutil.copy(lockfile, lockfile_orig)
> -
> -    newlines = []
> -    with open(lockfile_orig, "r") as f:
> -        for line in f.readlines():
> -            if not line.startswith("source = \"git"):
> -                newlines.append(line)
> -
> -    with open(lockfile, "w") as f:
> -        f.writelines(newlines)
>   }
> +
>   do_configure[postfuncs] += "cargo_common_do_patch_paths"
>   
>   do_compile:prepend () {
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
Gyorgy Sarvari Oct. 8, 2025, 11:01 a.m. UTC | #5
On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>> either a crate name that can be fetched from some registry (like crates.io), or
>> as a source crate, which is most often fetched from a git repository.
>>
>> Normally cargo handles fetching the crates from both the registry and from git,
>> however with Yocto this task is taken over by Bitbake.
>>
>> After fetching these crates, they are made available to cargo by adding the location
>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>> that can be found in the SRC_URI is added as one source crate.
>>
>> This works most of the time, as long as the repository really contains one crate only.
>>
>> However in case the repository is a cargo workspace, it contains multiple crates in
>> different subfolders, and in order to allow cargo to process them, they need to be
>> listed separately. This is not happening with the current implementation of cargo_common.
>>
>> This change introduces the following:
>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>    this was that maturin seems to ignore source crate patches from config.toml)
>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>    deleted git repository lines from it)
>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>    the separate crate folders are copied into this folder, and it is used as the central
>>    vendoring folder. This is needed for source replacements: the folder that is used for
>>    vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>    has the name of the crate that it contains. Workspaces are not included here (unless the
>>    given manifest is a workspace AND a package at once)
>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>    to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>    Having destsuffix is still mandatory though.
>>
>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
> I use a similar approach for my Cargo.lock fetcher. In my case the code 
> finds the crate on the fly inside the a git repository because the 
> Cargo.lock doesn't contain the subpath.

By any chance, did you manage to solve the workspace problem? If you
have a working solution, feel free to submit it, I wouldn't mind if I
wouldn't have to debug mine :D

>
>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>
>> ---
>>   meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>   1 file changed, 108 insertions(+), 50 deletions(-)
>>
>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>> index c9eb2d09a5..79c1351298 100644
>> --- a/meta/classes-recipe/cargo_common.bbclass
>> +++ b/meta/classes-recipe/cargo_common.bbclass
>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>   python cargo_common_do_patch_paths() {
>>       import shutil
>>   
>> +    def is_rust_crate_folder(path):
>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>> +        return os.path.exists(cargo_toml_path)
>> +
>> +    def load_toml_file(toml_path):
>> +        import tomllib
>> +        with open(toml_path, 'rb') as f:
>> +            toml = tomllib.load(f)
>> +        return toml
>> +
>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>> +        for lf_repo in lockfile_repos.keys():
>> +            if repo in lf_repo and lf_repo.endswith(revision):
> Does this works if the URL contains a "rev" query parameter? This 
> happens if the same git repository is used with different revisions.

I *think* yes, since I query the revision from the fetcher, instead of
parsing it myself (and I use both the repo and revision for matching the
cargo.lock repos). But will test it specifically, and make it work if it
wouldn't work out of the box. Thanks for calling my attention on this.

>
>> +                lockfile_repos[lf_repo] = True
>> +                return lf_repo.split("#")[0]
>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>> +
>> +    def create_cargo_checksum(folder_path):
>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>> +        if os.path.exists(checksum_path):
>> +            return
>> +
>> +        import hashlib, json
>> +
>> +        checksum = {'files': {}}
>> +        for root, _, files in os.walk(folder_path):
>> +            for f in files:
>> +                full_path = os.path.join(root, f)
>> +                relative_path = os.path.relpath(full_path, folder_path)
>> +                if relative_path.startswith(".git/"):
>> +                    continue
>> +                with open(full_path, 'rb') as f2:
>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>> +                checksum["files"][relative_path] = file_sha
> Do we really need the calculation of the checksum?

For source replacement AFAIK it is mandatory, otherwise cargo complains.
(But I'd be happy to stand corrected)

>> +
>> +        with open(checksum_path, 'w') as f:
>> +            json.dump(checksum, f)
>> +
>>       cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>       if not os.path.exists(cargo_config):
>>           return
>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>       if len(src_uri) == 0:
>>           return
>>   
>> -    patches = dict()
>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>> +    if not os.path.exists(lockfile):
>> +        bb.fatal(f"{lockfile} file doesn't exist")
>> +
>> +    lockfile = load_toml_file(lockfile)
>> +
>> +    # key is the repo url, value is a boolean, which is used later
>> +    # to indicate if there is a matching repository in SRC_URI also
>> +    lockfile_git_repos = {}
>> +    for p in lockfile['package']:
>> +        if 'source' in p and p['source'].startswith('git+'):
>> +            lockfile_git_repos[p['source']] = False
>> +
>> +    sources = dict()
>>       workdir = d.getVar('UNPACKDIR')
>>       fetcher = bb.fetch2.Fetch(src_uri, d)
>> +
>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>> +
>> +    os.makedirs(vendor_folder)
>> +
>>       for url in fetcher.urls:
>>           ud = fetcher.ud[url]
>> -        if ud.type == 'git' or ud.type == 'gitsm':
>> -            name = ud.parm.get('name')
>> -            destsuffix = ud.parm.get('destsuffix')
>> -            if name is not None and destsuffix is not None:
>> -                if ud.user:
>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>> -                else:
>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>> -                patches.setdefault(repo, []).append(path)
>> +        if ud.type != 'git' and ud.type != 'gitsm':
>> +            continue
>>   
>> -    with open(cargo_config, "a+") as config:
>> -        for k, v in patches.items():
>> -            print('\n[patch."%s"]' % k, file=config)
>> -            for name in v:
>> -                print(name, file=config)
>> +        destsuffix = ud.parm.get('destsuffix')
>> +        crate_folder = os.path.join(workdir, destsuffix)
>>   
>> -    if not patches:
>> -        return
>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>> +            continue
>>   
>> -    # Cargo.lock file is needed for to be sure that artifacts
>> -    # downloaded by the fetch steps are those expected by the
>> -    # project and that the possible patches are correctly applied.
>> -    # Moreover since we do not want any modification
>> -    # of this file (for reproducibility purpose), we prevent it by
>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>> -    # here is better than letting cargo tell (in case the file is missing)
>> -    # "Cargo.lock should be modified but --frozen was given"
>> +        if ud.user:
>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>> +        else:
>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>   
>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>> -    if not os.path.exists(lockfile):
>> -        bb.fatal(f"{lockfile} file doesn't exist")
>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>> +
>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>> +        cargo_toml = load_toml_file(cargo_toml_path)
>> +
>> +        if 'workspace' in cargo_toml:
>> +            members = cargo_toml['workspace']['members']
>> +            for member in members:
>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>> +                member_crate_name = member_cargo_toml['package']['name']
>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>> +
>> +        if 'package' in cargo_toml:
>> +            crate_folder = os.path.join(workdir, destsuffix)
>> +            crate_name = cargo_toml['package']['name']
>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>> +
>> +    for d in os.scandir(vendor_folder):
>> +        if d.is_dir():
>> +            create_cargo_checksum(d.path)
>> +
>> +
>> +    with open(cargo_config, "a+") as config:
>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>> +        print('directory = "%s"' % vendor_folder, file=config)
>> +
>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>> +            print('git = "%s"' % repo, file=config)
>> +            print('rev = "%s"' % revision, file=config)
>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>> +
>> +    # check if there are any git repos in the lock file that were not visited
>> +    # in the previous loop, when the source replacement was created, and warn about it
>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>> +        if not found_in_src_uri:
>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>   
>> -    # There are patched files and so Cargo.lock should be modified but we use
>> -    # --frozen so let's handle that modifications here.
>> -    #
>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>> -    # patched packages:
>> -    #  cargo update --offline -p package_1 -p package_2
>> -    # But this is not possible since it requires that cargo local git db
>> -    # to be populated and this is not the case as we fetch git repo ourself.
>> -
>> -    lockfile_orig = lockfile + ".orig"
>> -    if not os.path.exists(lockfile_orig):
>> -        shutil.copy(lockfile, lockfile_orig)
>> -
>> -    newlines = []
>> -    with open(lockfile_orig, "r") as f:
>> -        for line in f.readlines():
>> -            if not line.startswith("source = \"git"):
>> -                newlines.append(line)
>> -
>> -    with open(lockfile, "w") as f:
>> -        f.writelines(newlines)
>>   }
>> +
>>   do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>   
>>   do_compile:prepend () {
>>
>> -=-=-=-=-=-=-=-=-=-=-=-
>> Links: You receive all messages sent to this group.
>> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
>> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
>> Group Owner: openembedded-core+owner@lists.openembedded.org
>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>> -=-=-=-=-=-=-=-=-=-=-=-
>>
Stefan Herbrechtsmeier Oct. 9, 2025, 9:31 a.m. UTC | #6
Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>> either a crate name that can be fetched from some registry (like crates.io), or
>>> as a source crate, which is most often fetched from a git repository.
>>>
>>> Normally cargo handles fetching the crates from both the registry and from git,
>>> however with Yocto this task is taken over by Bitbake.
>>>
>>> After fetching these crates, they are made available to cargo by adding the location
>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>> that can be found in the SRC_URI is added as one source crate.
>>>
>>> This works most of the time, as long as the repository really contains one crate only.
>>>
>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>> different subfolders, and in order to allow cargo to process them, they need to be
>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>
>>> This change introduces the following:
>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>     this was that maturin seems to ignore source crate patches from config.toml)
>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>     deleted git repository lines from it)
>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>     the separate crate folders are copied into this folder, and it is used as the central
>>>     vendoring folder. This is needed for source replacements: the folder that is used for
>>>     vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>     has the name of the crate that it contains. Workspaces are not included here (unless the
>>>     given manifest is a workspace AND a package at once)
>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>     to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>     Having destsuffix is still mandatory though.
>>>
>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>> I use a similar approach for my Cargo.lock fetcher. In my case the code
>> finds the crate on the fly inside the a git repository because the
>> Cargo.lock doesn't contain the subpath.
> By any chance, did you manage to solve the workspace problem? If you
> have a working solution, feel free to submit it, I wouldn't mind if I
> wouldn't have to debug mine :D
I haven't test a workspace project. Do you have an example project?

>
>>> Signed-off-by: Gyorgy Sarvari<skandigraun@gmail.com>
>>> Cc: Tom Geelen<t.f.g.geelen@gmail.com>
>>>
>>> ---
>>>    meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>    1 file changed, 108 insertions(+), 50 deletions(-)
>>>
>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>> index c9eb2d09a5..79c1351298 100644
>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>    python cargo_common_do_patch_paths() {
>>>        import shutil
>>>    
>>> +    def is_rust_crate_folder(path):
>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>> +        return os.path.exists(cargo_toml_path)
>>> +
>>> +    def load_toml_file(toml_path):
>>> +        import tomllib
>>> +        with open(toml_path, 'rb') as f:
>>> +            toml = tomllib.load(f)
>>> +        return toml
>>> +
>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>> +        for lf_repo in lockfile_repos.keys():
>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>> Does this works if the URL contains a "rev" query parameter? This
>> happens if the same git repository is used with different revisions.
> I *think* yes, since I query the revision from the fetcher, instead of
> parsing it myself (and I use both the repo and revision for matching the
> cargo.lock repos). But will test it specifically, and make it work if it
> wouldn't work out of the box. Thanks for calling my attention on this.
The problem is that the source replacement key contains a query 
parameter. The query isn't supported by the git fetcher. That means you 
have to remove the query from the SRC_URI but add it back in the source 
entry in the config.toml.

>>> +                lockfile_repos[lf_repo] = True
>>> +                return lf_repo.split("#")[0]
>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>> +
>>> +    def create_cargo_checksum(folder_path):
>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>> +        if os.path.exists(checksum_path):
>>> +            return
>>> +
>>> +        import hashlib, json
>>> +
>>> +        checksum = {'files': {}}
>>> +        for root, _, files in os.walk(folder_path):
>>> +            for f in files:
>>> +                full_path = os.path.join(root, f)
>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>> +                if relative_path.startswith(".git/"):
>>> +                    continue
>>> +                with open(full_path, 'rb') as f2:
>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>> +                checksum["files"][relative_path] = file_sha
>> Do we really need the calculation of the checksum?
> For source replacement AFAIK it is mandatory, otherwise cargo complains.
> (But I'd be happy to stand corrected)
Have you test an empty dictionary for "files" and NULL for "package"?

>>> +
>>> +        with open(checksum_path, 'w') as f:
>>> +            json.dump(checksum, f)
>>> +
>>>        cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>        if not os.path.exists(cargo_config):
>>>            return
>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>        if len(src_uri) == 0:
>>>            return
>>>    
>>> -    patches = dict()
>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>> +    if not os.path.exists(lockfile):
>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>> +
>>> +    lockfile = load_toml_file(lockfile)
>>> +
>>> +    # key is the repo url, value is a boolean, which is used later
>>> +    # to indicate if there is a matching repository in SRC_URI also
>>> +    lockfile_git_repos = {}
>>> +    for p in lockfile['package']:
>>> +        if 'source' in p and p['source'].startswith('git+'):
>>> +            lockfile_git_repos[p['source']] = False
>>> +
>>> +    sources = dict()
>>>        workdir = d.getVar('UNPACKDIR')
>>>        fetcher = bb.fetch2.Fetch(src_uri, d)
>>> +
>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>> +
>>> +    os.makedirs(vendor_folder)
>>> +
>>>        for url in fetcher.urls:
>>>            ud = fetcher.ud[url]
>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>> -            name = ud.parm.get('name')
>>> -            destsuffix = ud.parm.get('destsuffix')
>>> -            if name is not None and destsuffix is not None:
>>> -                if ud.user:
>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>> -                else:
>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>> -                patches.setdefault(repo, []).append(path)
>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>> +            continue
>>>    
>>> -    with open(cargo_config, "a+") as config:
>>> -        for k, v in patches.items():
>>> -            print('\n[patch."%s"]' % k, file=config)
>>> -            for name in v:
>>> -                print(name, file=config)
>>> +        destsuffix = ud.parm.get('destsuffix')
>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>    
>>> -    if not patches:
>>> -        return
>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>> +            continue
>>>    
>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>> -    # downloaded by the fetch steps are those expected by the
>>> -    # project and that the possible patches are correctly applied.
>>> -    # Moreover since we do not want any modification
>>> -    # of this file (for reproducibility purpose), we prevent it by
>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>> -    # here is better than letting cargo tell (in case the file is missing)
>>> -    # "Cargo.lock should be modified but --frozen was given"
>>> +        if ud.user:
>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>> +        else:
>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>    
>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>> -    if not os.path.exists(lockfile):
>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>> +
>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>> +
>>> +        if 'workspace' in cargo_toml:
>>> +            members = cargo_toml['workspace']['members']
>>> +            for member in members:
>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>> +                member_crate_name = member_cargo_toml['package']['name']
>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>> +
>>> +        if 'package' in cargo_toml:
>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>> +            crate_name = cargo_toml['package']['name']
>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>> +
>>> +    for d in os.scandir(vendor_folder):
>>> +        if d.is_dir():
>>> +            create_cargo_checksum(d.path)
>>> +
>>> +
>>> +    with open(cargo_config, "a+") as config:
>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>> +
>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>> +            print('git = "%s"' % repo, file=config)
>>> +            print('rev = "%s"' % revision, file=config)
>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>> +
>>> +    # check if there are any git repos in the lock file that were not visited
>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>> +        if not found_in_src_uri:
>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>    
>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>> -    # --frozen so let's handle that modifications here.
>>> -    #
>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>> -    # patched packages:
>>> -    #  cargo update --offline -p package_1 -p package_2
>>> -    # But this is not possible since it requires that cargo local git db
>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>> -
>>> -    lockfile_orig = lockfile + ".orig"
>>> -    if not os.path.exists(lockfile_orig):
>>> -        shutil.copy(lockfile, lockfile_orig)
>>> -
>>> -    newlines = []
>>> -    with open(lockfile_orig, "r") as f:
>>> -        for line in f.readlines():
>>> -            if not line.startswith("source = \"git"):
>>> -                newlines.append(line)
>>> -
>>> -    with open(lockfile, "w") as f:
>>> -        f.writelines(newlines)
>>>    }
>>> +
>>>    do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>    
>>>    do_compile:prepend () {
>>>
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>> Links: You receive all messages sent to this group.
>>> View/Reply Online (#224426):https://lists.openembedded.org/g/openembedded-core/message/224426
>>> Mute This Topic:https://lists.openembedded.org/mt/115578466/6374899
>>> Group Owner:openembedded-core+owner@lists.openembedded.org
>>> Unsubscribe:https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>
Yash Shinde Oct. 9, 2025, 12:18 p.m. UTC | #7
On Sat, Oct 4, 2025 at 03:00 AM, Gyorgy Sarvari wrote:

> 
> Cargo.toml files usually contain a list of dependencies in one of two
> forms:
> either a crate name that can be fetched from some registry (like
> crates.io), or
> as a source crate, which is most often fetched from a git repository.
> 
> Normally cargo handles fetching the crates from both the registry and from
> git,
> however with Yocto this task is taken over by Bitbake.
> 
> After fetching these crates, they are made available to cargo by adding
> the location
> to $CARGO_HOME/config.toml. The source crates are of interest here: each
> git repository
> that can be found in the SRC_URI is added as one source crate.
> 
> This works most of the time, as long as the repository really contains one
> crate only.
> 
> However in case the repository is a cargo workspace, it contains multiple
> crates in
> different subfolders, and in order to allow cargo to process them, they
> need to be
> listed separately. This is not happening with the current implementation
> of cargo_common.
> 
> This change introduces the following:
> - instead of patching the dependencies, use source replacement (the
> primary motivation for
> this was that maturin seems to ignore source crate patches from
> config.toml)
> - the above also allows to keep the original Cargo.lock untouched (the
> original implementation
> deleted git repository lines from it)
> - it adds a new folder, currently
> ${UNPACKDIR}/yocto-vendored-source-crates. During processing
> the separate crate folders are copied into this folder, and it is used as
> the central
> vendoring folder. This is needed for source replacements: the folder that
> is used for
> vendoring needs to contain the crates separately, one crate in one folder.
> Each folder
> has the name of the crate that it contains. Workspaces are not included
> here (unless the
> given manifest is a workspace AND a package at once)
> - previuosly the SRC_URI had to contain a "name" and a "destsuffix"
> parameter to be considered
> to be a rust crate. The name is not derived from the Cargo.toml file, not
> from the SRC_URI.
> Having destsuffix is still mandatory though.
> 
> The change does not handle nested workspaces, only the top level
> Cargo.toml is processed.

I would like to understand more about this transition.
Does this patch address any regression or bug? If so, please provide more details.
As mentioned it is meant of maturin crate to handle the patches.
Can this be done at the crate level?

The python errors can be fixed by replacing tomlib (deprecated from python 3.14 ) with tomli.

The Cargo.lock seems to missing from sysroot and breaks the build after this patch.
ERROR: libstd-rs-1.90.0-r0 do_configure: /home/user/poky/build/tmp/work/x86-64-v3-poky-linux/libstd-rs/1.90.0/sources/rustc-1.90.0-src/library/sysroot/Cargo.lock file doesn't exist

> 
> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
> 
> ---
> meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
> 1 file changed, 108 insertions(+), 50 deletions(-)
> 
> diff --git a/meta/classes-recipe/cargo_common.bbclass
> b/meta/classes-recipe/cargo_common.bbclass
> index c9eb2d09a5..79c1351298 100644
> --- a/meta/classes-recipe/cargo_common.bbclass
> +++ b/meta/classes-recipe/cargo_common.bbclass
> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
> python cargo_common_do_patch_paths() {
> import shutil
> 
> + def is_rust_crate_folder(path):
> + cargo_toml_path = os.path.join(path, 'Cargo.toml')
> + return os.path.exists(cargo_toml_path)
> +
> + def load_toml_file(toml_path):
> + import tomllib
> + with open(toml_path, 'rb') as f:
> + toml = tomllib.load(f)

Use tomli here.

> 
> + return toml
> +
> + def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
> + for lf_repo in lockfile_repos.keys():
> + if repo in lf_repo and lf_repo.endswith(revision):
> + lockfile_repos[lf_repo] = True
> + return lf_repo.split("#")[0]
> + bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock
> file' % (repo, revision))
> +
> + def create_cargo_checksum(folder_path):
> + checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
> + if os.path.exists(checksum_path):
> + return
> +
> + import hashlib, json
> +
> + checksum = {'files': {}}
> + for root, _, files in os.walk(folder_path):
> + for f in files:
> + full_path = os.path.join(root, f)
> + relative_path = os.path.relpath(full_path, folder_path)
> + if relative_path.startswith(".git/"):
> + continue
> + with open(full_path, 'rb') as f2:
> + file_sha = hashlib.sha256(f2.read()).hexdigest()
> + checksum["files"][relative_path] = file_sha
> +
> + with open(checksum_path, 'w') as f:
> + json.dump(checksum, f)
> +
> cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
> if not os.path.exists(cargo_config):
> return
> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
> if len(src_uri) == 0:
> return
> 
> - patches = dict()
> + lockfile = d.getVar("CARGO_LOCK_PATH")
> + if not os.path.exists(lockfile):
> + bb.fatal(f"{lockfile} file doesn't exist")
> +
> + lockfile = load_toml_file(lockfile)
> +
> + # key is the repo url, value is a boolean, which is used later
> + # to indicate if there is a matching repository in SRC_URI also
> + lockfile_git_repos = {}
> + for p in lockfile['package']:
> + if 'source' in p and p['source'].startswith('git+'):
> + lockfile_git_repos[p['source']] = False
> +
> + sources = dict()
> workdir = d.getVar('UNPACKDIR')
> fetcher = bb.fetch2.Fetch(src_uri, d)
> +
> + vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
> +
> + os.makedirs(vendor_folder)
> +
> for url in fetcher.urls:
> ud = fetcher.ud[url]
> - if ud.type == 'git' or ud.type == 'gitsm':
> - name = ud.parm.get('name')
> - destsuffix = ud.parm.get('destsuffix')
> - if name is not None and destsuffix is not None:
> - if ud.user:
> - repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
> - else:
> - repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
> - path = '%s = { path = "%s" }' % (name, os.path.join(workdir,
> destsuffix))
> - patches.setdefault(repo, []).append(path)
> + if ud.type != 'git' and ud.type != 'gitsm':
> + continue
> 
> - with open(cargo_config, "a+") as config:
> - for k, v in patches.items():
> - print('\n[patch."%s"]' % k, file=config)
> - for name in v:
> - print(name, file=config)
> + destsuffix = ud.parm.get('destsuffix')
> + crate_folder = os.path.join(workdir, destsuffix)
> 
> - if not patches:
> - return
> + if destsuffix is None or not is_rust_crate_folder(crate_folder):
> + continue
> 
> - # Cargo.lock file is needed for to be sure that artifacts
> - # downloaded by the fetch steps are those expected by the
> - # project and that the possible patches are correctly applied.
> - # Moreover since we do not want any modification
> - # of this file (for reproducibility purpose), we prevent it by
> - # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
> - # here is better than letting cargo tell (in case the file is missing)
> - # "Cargo.lock should be modified but --frozen was given"
> + if ud.user:
> + repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
> + else:
> + repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
> 
> - lockfile = d.getVar("CARGO_LOCK_PATH")
> - if not os.path.exists(lockfile):
> - bb.fatal(f"{lockfile} file doesn't exist")
> + sources[destsuffix] = (repo, ud.revision, crate_folder)
> +
> + cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
> + cargo_toml = load_toml_file(cargo_toml_path)
> +
> + if 'workspace' in cargo_toml:
> + members = cargo_toml['workspace']['members']
> + for member in members:
> + member_crate_folder = os.path.join(workdir, destsuffix, member)
> + member_crate_cargo_toml = os.path.join(member_crate_folder,
> 'Cargo.toml')
> + member_cargo_toml = load_toml_file(member_crate_cargo_toml)
> + member_crate_name = member_cargo_toml['package']['name']
> + shutil.copytree(member_crate_folder, os.path.join(vendor_folder,
> member_crate_name))
> +
> + if 'package' in cargo_toml:
> + crate_folder = os.path.join(workdir, destsuffix)
> + crate_name = cargo_toml['package']['name']
> + shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
> +
> + for d in os.scandir(vendor_folder):
> + if d.is_dir():
> + create_cargo_checksum(d.path)
> +
> +
> + with open(cargo_config, "a+") as config:
> + print('\n[source."yocto-vendored-sources"]', file=config)
> + print('directory = "%s"' % vendor_folder, file=config)
> +
> + for destsuffix, (repo, revision, repo_path) in sources.items():
> + lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos,
> repo, revision)
> + print('\n[source."%s"]' % lockfile_repo, file=config)
> + print('git = "%s"' % repo, file=config)
> + print('rev = "%s"' % revision, file=config)
> + print('replace-with = "yocto-vendored-sources"', file=config)
> +
> + # check if there are any git repos in the lock file that were not
> visited
> + # in the previous loop, when the source replacement was created, and
> warn about it
> + for lf_repo, found_in_src_uri in lockfile_git_repos.items():
> + if not found_in_src_uri:
> + bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
> 
> - # There are patched files and so Cargo.lock should be modified but we
> use
> - # --frozen so let's handle that modifications here.
> - #
> - # Note that a "better" (more elegant ?) would have been to use cargo
> update for
> - # patched packages:
> - # cargo update --offline -p package_1 -p package_2
> - # But this is not possible since it requires that cargo local git db
> - # to be populated and this is not the case as we fetch git repo ourself.
> 
> -
> - lockfile_orig = lockfile + ".orig"
> - if not os.path.exists(lockfile_orig):
> - shutil.copy(lockfile, lockfile_orig)
> -
> - newlines = []
> - with open(lockfile_orig, "r") as f:
> - for line in f.readlines():
> - if not line.startswith("source = \"git"):
> - newlines.append(line)
> -
> - with open(lockfile, "w") as f:
> - f.writelines(newlines)
> }
> +
> do_configure[postfuncs] += "cargo_common_do_patch_paths"
> 
> do_compile:prepend () {
Gyorgy Sarvari Oct. 9, 2025, 2:03 p.m. UTC | #8
On 10/9/25 14:18, Yash Shinde via lists.openembedded.org wrote:
> On Sat, Oct 4, 2025 at 03:00 AM, Gyorgy Sarvari wrote:
>
>     Cargo.toml files usually contain a list of dependencies in one of
>     two forms:
>     either a crate name that can be fetched from some registry (like
>     crates.io), or
>     as a source crate, which is most often fetched from a git repository.
>
>     Normally cargo handles fetching the crates from both the registry
>     and from git,
>     however with Yocto this task is taken over by Bitbake.
>
>     After fetching these crates, they are made available to cargo by
>     adding the location
>     to $CARGO_HOME/config.toml. The source crates are of interest
>     here: each git repository
>     that can be found in the SRC_URI is added as one source crate.
>
>     This works most of the time, as long as the repository really
>     contains one crate only.
>
>     However in case the repository is a cargo workspace, it contains
>     multiple crates in
>     different subfolders, and in order to allow cargo to process them,
>     they need to be
>     listed separately. This is not happening with the current
>     implementation of cargo_common.
>
>     This change introduces the following:
>     - instead of patching the dependencies, use source replacement
>     (the primary motivation for
>     this was that maturin seems to ignore source crate patches from
>     config.toml)
>     - the above also allows to keep the original Cargo.lock untouched
>     (the original implementation
>     deleted git repository lines from it)
>     - it adds a new folder, currently
>     ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>     the separate crate folders are copied into this folder, and it is
>     used as the central
>     vendoring folder. This is needed for source replacements: the
>     folder that is used for
>     vendoring needs to contain the crates separately, one crate in one
>     folder. Each folder
>     has the name of the crate that it contains. Workspaces are not
>     included here (unless the
>     given manifest is a workspace AND a package at once)
>     - previuosly the SRC_URI had to contain a "name" and a
>     "destsuffix" parameter to be considered
>     to be a rust crate. The name is not derived from the Cargo.toml
>     file, not from the SRC_URI.
>     Having destsuffix is still mandatory though.
>
>     The change does not handle nested workspaces, only the top level
>     Cargo.toml is processed.
>
> I would like to understand more about this transition.
> Does this patch address any regression or bug? If so, please provide
> more details.

Thanks a lot for the review.
In case the Cargo project declares a dependency that is fetched from a
git repository, and that dependency is a workspace, then the build fails
(this is true for any cargo project with a workspace git dependency).

When cargo_common patches config.toml it assumes that a repository
contains only 1 crate, and that crate's manifest is in the top folder
always.

With workspaces this might not be the case, they can contain unlimited
number of crates in subfolders, each with its own manifest. In that case
after patching config.toml points to the parent folder of the
sub-crates, however for dependency patching each crates from the
workspace should be mentioned in the patch separately, otherwise cargo
complains that it can't find a crate manifest, only virtual (workspace)
manifests.

The main goal would be to handle this case.

> As mentioned it is meant of maturin crate to handle the patches.
> Can this be done at the crate level?
>

The core-problem with workspaces is a generic issue. My original
patch[1] was much simpler and just extended the existing dependency
patching by adding each crate separately, however maturin didn't pick it
up for some reason. Could be a skill issue on my end - if so, I wouldn't
argue against having simpler code.

[1]: https://lists.openembedded.org/g/openembedded-core/message/224388

> The python errors can be fixed by replacing tomlib (deprecated from
> python 3.14 ) with tomli.

tomli would be a new external dependency, if I'm not mistaken. I
wouldn't argue for a new requirement for this (though if someone else
would, that would make life easier).

>
> The Cargo.lock seems to missing from sysroot and breaks the build
> after this patch.
> ERROR: libstd-rs-1.90.0-r0 do_configure:
> /home/user/poky/build/tmp/work/x86-64-v3-poky-linux/libstd-rs/1.90.0/sources/rustc-1.90.0-src/library/sysroot/Cargo.lock
> file doesn't exist

Thanks for the report! I missed this, but V2 should address it, which
I'm about to send out.

>
>
>     Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>     Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>
>     ---
>     meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>     1 file changed, 108 insertions(+), 50 deletions(-)
>
>     diff --git a/meta/classes-recipe/cargo_common.bbclass
>     b/meta/classes-recipe/cargo_common.bbclass
>     index c9eb2d09a5..79c1351298 100644
>     --- a/meta/classes-recipe/cargo_common.bbclass
>     +++ b/meta/classes-recipe/cargo_common.bbclass
>     @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>     python cargo_common_do_patch_paths() {
>     import shutil
>
>     + def is_rust_crate_folder(path):
>     + cargo_toml_path = os.path.join(path, 'Cargo.toml')
>     + return os.path.exists(cargo_toml_path)
>     +
>     + def load_toml_file(toml_path):
>     + import tomllib
>     + with open(toml_path, 'rb') as f:
>     + toml = tomllib.load(f)
>
> Use tomli here.
>
>     + return toml
>     +
>     + def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>     + for lf_repo in lockfile_repos.keys():
>     + if repo in lf_repo and lf_repo.endswith(revision):
>     + lockfile_repos[lf_repo] = True
>     + return lf_repo.split("#")[0]
>     + bb.fatal('Cannot find %s (%s) repository from SRC_URI in
>     Cargo.lock file' % (repo, revision))
>     +
>     + def create_cargo_checksum(folder_path):
>     + checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>     + if os.path.exists(checksum_path):
>     + return
>     +
>     + import hashlib, json
>     +
>     + checksum = {'files': {}}
>     + for root, _, files in os.walk(folder_path):
>     + for f in files:
>     + full_path = os.path.join(root, f)
>     + relative_path = os.path.relpath(full_path, folder_path)
>     + if relative_path.startswith(".git/"):
>     + continue
>     + with open(full_path, 'rb') as f2:
>     + file_sha = hashlib.sha256(f2.read()).hexdigest()
>     + checksum["files"][relative_path] = file_sha
>     +
>     + with open(checksum_path, 'w') as f:
>     + json.dump(checksum, f)
>     +
>     cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>     if not os.path.exists(cargo_config):
>     return
>     @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>     if len(src_uri) == 0:
>     return
>
>     - patches = dict()
>     + lockfile = d.getVar("CARGO_LOCK_PATH")
>     + if not os.path.exists(lockfile):
>     + bb.fatal(f"{lockfile} file doesn't exist")
>     +
>     + lockfile = load_toml_file(lockfile)
>     +
>     + # key is the repo url, value is a boolean, which is used later
>     + # to indicate if there is a matching repository in SRC_URI also
>     + lockfile_git_repos = {}
>     + for p in lockfile['package']:
>     + if 'source' in p and p['source'].startswith('git+'):
>     + lockfile_git_repos[p['source']] = False
>     +
>     + sources = dict()
>     workdir = d.getVar('UNPACKDIR')
>     fetcher = bb.fetch2.Fetch(src_uri, d)
>     +
>     + vendor_folder = os.path.join(workdir,
>     'yocto-vendored-source-crates')
>     +
>     + os.makedirs(vendor_folder)
>     +
>     for url in fetcher.urls:
>     ud = fetcher.ud[url]
>     - if ud.type == 'git' or ud.type == 'gitsm':
>     - name = ud.parm.get('name')
>     - destsuffix = ud.parm.get('destsuffix')
>     - if name is not None and destsuffix is not None:
>     - if ud.user:
>     - repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>     - else:
>     - repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>     - path = '%s = { path = "%s" }' % (name, os.path.join(workdir,
>     destsuffix))
>     - patches.setdefault(repo, []).append(path)
>     + if ud.type != 'git' and ud.type != 'gitsm':
>     + continue
>
>     - with open(cargo_config, "a+") as config:
>     - for k, v in patches.items():
>     - print('\n[patch."%s"]' % k, file=config)
>     - for name in v:
>     - print(name, file=config)
>     + destsuffix = ud.parm.get('destsuffix')
>     + crate_folder = os.path.join(workdir, destsuffix)
>
>     - if not patches:
>     - return
>     + if destsuffix is None or not is_rust_crate_folder(crate_folder):
>     + continue
>
>     - # Cargo.lock file is needed for to be sure that artifacts
>     - # downloaded by the fetch steps are those expected by the
>     - # project and that the possible patches are correctly applied.
>     - # Moreover since we do not want any modification
>     - # of this file (for reproducibility purpose), we prevent it by
>     - # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>     - # here is better than letting cargo tell (in case the file is
>     missing)
>     - # "Cargo.lock should be modified but --frozen was given"
>     + if ud.user:
>     + repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>     + else:
>     + repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>
>     - lockfile = d.getVar("CARGO_LOCK_PATH")
>     - if not os.path.exists(lockfile):
>     - bb.fatal(f"{lockfile} file doesn't exist")
>     + sources[destsuffix] = (repo, ud.revision, crate_folder)
>     +
>     + cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>     + cargo_toml = load_toml_file(cargo_toml_path)
>     +
>     + if 'workspace' in cargo_toml:
>     + members = cargo_toml['workspace']['members']
>     + for member in members:
>     + member_crate_folder = os.path.join(workdir, destsuffix, member)
>     + member_crate_cargo_toml = os.path.join(member_crate_folder,
>     'Cargo.toml')
>     + member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>     + member_crate_name = member_cargo_toml['package']['name']
>     + shutil.copytree(member_crate_folder, os.path.join(vendor_folder,
>     member_crate_name))
>     +
>     + if 'package' in cargo_toml:
>     + crate_folder = os.path.join(workdir, destsuffix)
>     + crate_name = cargo_toml['package']['name']
>     + shutil.copytree(crate_folder, os.path.join(vendor_folder,
>     crate_name))
>     +
>     + for d in os.scandir(vendor_folder):
>     + if d.is_dir():
>     + create_cargo_checksum(d.path)
>     +
>     +
>     + with open(cargo_config, "a+") as config:
>     + print('\n[source."yocto-vendored-sources"]', file=config)
>     + print('directory = "%s"' % vendor_folder, file=config)
>     +
>     + for destsuffix, (repo, revision, repo_path) in sources.items():
>     + lockfile_repo =
>     get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>     + print('\n[source."%s"]' % lockfile_repo, file=config)
>     + print('git = "%s"' % repo, file=config)
>     + print('rev = "%s"' % revision, file=config)
>     + print('replace-with = "yocto-vendored-sources"', file=config)
>     +
>     + # check if there are any git repos in the lock file that were
>     not visited
>     + # in the previous loop, when the source replacement was created,
>     and warn about it
>     + for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>     + if not found_in_src_uri:
>     + bb.warn(f"{lf_repo} is present in lockfile, but not found in
>     SRC_URI")
>
>     - # There are patched files and so Cargo.lock should be modified
>     but we use
>     - # --frozen so let's handle that modifications here.
>     - #
>     - # Note that a "better" (more elegant ?) would have been to use
>     cargo update for
>     - # patched packages:
>     - # cargo update --offline -p package_1 -p package_2
>     - # But this is not possible since it requires that cargo local git db
>     - # to be populated and this is not the case as we fetch git repo
>     ourself.
>     -
>     - lockfile_orig = lockfile + ".orig"
>     - if not os.path.exists(lockfile_orig):
>     - shutil.copy(lockfile, lockfile_orig)
>     -
>     - newlines = []
>     - with open(lockfile_orig, "r") as f:
>     - for line in f.readlines():
>     - if not line.startswith("source = \"git"):
>     - newlines.append(line)
>     -
>     - with open(lockfile, "w") as f:
>     - f.writelines(newlines)
>     }
>     +
>     do_configure[postfuncs] += "cargo_common_do_patch_paths"
>
>     do_compile:prepend () {
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#224612): https://lists.openembedded.org/g/openembedded-core/message/224612
> Mute This Topic: https://lists.openembedded.org/mt/115578466/6084445
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [skandigraun@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
Gyorgy Sarvari Oct. 9, 2025, 2:30 p.m. UTC | #9
On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>> as a source crate, which is most often fetched from a git repository.
>>>>
>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>> however with Yocto this task is taken over by Bitbake.
>>>>
>>>> After fetching these crates, they are made available to cargo by adding the location
>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>> that can be found in the SRC_URI is added as one source crate.
>>>>
>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>
>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>
>>>> This change introduces the following:
>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>    this was that maturin seems to ignore source crate patches from config.toml)
>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>    deleted git repository lines from it)
>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>    the separate crate folders are copied into this folder, and it is used as the central
>>>>    vendoring folder. This is needed for source replacements: the folder that is used for
>>>>    vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>    has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>    given manifest is a workspace AND a package at once)
>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>    to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>    Having destsuffix is still mandatory though.
>>>>
>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>> I use a similar approach for my Cargo.lock fetcher. In my case the code 
>>> finds the crate on the fly inside the a git repository because the 
>>> Cargo.lock doesn't contain the subpath.
>> By any chance, did you manage to solve the workspace problem? If you
>> have a working solution, feel free to submit it, I wouldn't mind if I
>> wouldn't have to debug mine :D
> I haven't test a workspace project. Do you have an example project?
>

I have attached a sample recipe (that is very much based on Tom Geelen's
initial work). It depends on at least 2 workspaces.

>>>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>>>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>>>
>>>> ---
>>>>   meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>   1 file changed, 108 insertions(+), 50 deletions(-)
>>>>
>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>> index c9eb2d09a5..79c1351298 100644
>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>   python cargo_common_do_patch_paths() {
>>>>       import shutil
>>>>   
>>>> +    def is_rust_crate_folder(path):
>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>> +        return os.path.exists(cargo_toml_path)
>>>> +
>>>> +    def load_toml_file(toml_path):
>>>> +        import tomllib
>>>> +        with open(toml_path, 'rb') as f:
>>>> +            toml = tomllib.load(f)
>>>> +        return toml
>>>> +
>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>> +        for lf_repo in lockfile_repos.keys():
>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>> Does this works if the URL contains a "rev" query parameter? This 
>>> happens if the same git repository is used with different revisions.
>> I *think* yes, since I query the revision from the fetcher, instead of
>> parsing it myself (and I use both the repo and revision for matching the
>> cargo.lock repos). But will test it specifically, and make it work if it
>> wouldn't work out of the box. Thanks for calling my attention on this.
> The problem is that the source replacement key contains a query
> parameter. The query isn't supported by the git fetcher. That means
> you have to remove the query from the SRC_URI but add it back in the
> source entry in the config.toml.

You mean for dynamic fetching, from Cargo.lock? This patch still relies
on the user adding these dependencies to the SRC_URI.
Otherwise I might be misunderstanding your question...

>
>>>> +                lockfile_repos[lf_repo] = True
>>>> +                return lf_repo.split("#")[0]
>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>> +
>>>> +    def create_cargo_checksum(folder_path):
>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>> +        if os.path.exists(checksum_path):
>>>> +            return
>>>> +
>>>> +        import hashlib, json
>>>> +
>>>> +        checksum = {'files': {}}
>>>> +        for root, _, files in os.walk(folder_path):
>>>> +            for f in files:
>>>> +                full_path = os.path.join(root, f)
>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>> +                if relative_path.startswith(".git/"):
>>>> +                    continue
>>>> +                with open(full_path, 'rb') as f2:
>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>> +                checksum["files"][relative_path] = file_sha
>>> Do we really need the calculation of the checksum?
>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>> (But I'd be happy to stand corrected)
> Have you test an empty dictionary for "files" and NULL for "package"?
>

Are these valid states? Currently the checksum calculation happens for
crate folders that have been actually copied to the vendor folder. And
that happens only, in case there is at least a Cargo.toml manifest in
that folder, so the files dict shouldn't be empty. Otherwise the
checksum sub iterates through all the files it can find, it doesn't try
to validate it against any manifests.

>>>> +
>>>> +        with open(checksum_path, 'w') as f:
>>>> +            json.dump(checksum, f)
>>>> +
>>>>       cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>       if not os.path.exists(cargo_config):
>>>>           return
>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>       if len(src_uri) == 0:
>>>>           return
>>>>   
>>>> -    patches = dict()
>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>> +    if not os.path.exists(lockfile):
>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>> +
>>>> +    lockfile = load_toml_file(lockfile)
>>>> +
>>>> +    # key is the repo url, value is a boolean, which is used later
>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>> +    lockfile_git_repos = {}
>>>> +    for p in lockfile['package']:
>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>> +            lockfile_git_repos[p['source']] = False
>>>> +
>>>> +    sources = dict()
>>>>       workdir = d.getVar('UNPACKDIR')
>>>>       fetcher = bb.fetch2.Fetch(src_uri, d)
>>>> +
>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>> +
>>>> +    os.makedirs(vendor_folder)
>>>> +
>>>>       for url in fetcher.urls:
>>>>           ud = fetcher.ud[url]
>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>> -            name = ud.parm.get('name')
>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>> -            if name is not None and destsuffix is not None:
>>>> -                if ud.user:
>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>> -                else:
>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>> -                patches.setdefault(repo, []).append(path)
>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>> +            continue
>>>>   
>>>> -    with open(cargo_config, "a+") as config:
>>>> -        for k, v in patches.items():
>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>> -            for name in v:
>>>> -                print(name, file=config)
>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>   
>>>> -    if not patches:
>>>> -        return
>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>> +            continue
>>>>   
>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>> -    # downloaded by the fetch steps are those expected by the
>>>> -    # project and that the possible patches are correctly applied.
>>>> -    # Moreover since we do not want any modification
>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>> +        if ud.user:
>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>> +        else:
>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>   
>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>> -    if not os.path.exists(lockfile):
>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>> +
>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>> +
>>>> +        if 'workspace' in cargo_toml:
>>>> +            members = cargo_toml['workspace']['members']
>>>> +            for member in members:
>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>> +
>>>> +        if 'package' in cargo_toml:
>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>> +            crate_name = cargo_toml['package']['name']
>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>> +
>>>> +    for d in os.scandir(vendor_folder):
>>>> +        if d.is_dir():
>>>> +            create_cargo_checksum(d.path)
>>>> +
>>>> +
>>>> +    with open(cargo_config, "a+") as config:
>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>> +
>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>> +            print('git = "%s"' % repo, file=config)
>>>> +            print('rev = "%s"' % revision, file=config)
>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>> +
>>>> +    # check if there are any git repos in the lock file that were not visited
>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>> +        if not found_in_src_uri:
>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>   
>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>> -    # --frozen so let's handle that modifications here.
>>>> -    #
>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>> -    # patched packages:
>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>> -    # But this is not possible since it requires that cargo local git db
>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>> -
>>>> -    lockfile_orig = lockfile + ".orig"
>>>> -    if not os.path.exists(lockfile_orig):
>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>> -
>>>> -    newlines = []
>>>> -    with open(lockfile_orig, "r") as f:
>>>> -        for line in f.readlines():
>>>> -            if not line.startswith("source = \"git"):
>>>> -                newlines.append(line)
>>>> -
>>>> -    with open(lockfile, "w") as f:
>>>> -        f.writelines(newlines)
>>>>   }
>>>> +
>>>>   do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>   
>>>>   do_compile:prepend () {
>>>>
>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>> Links: You receive all messages sent to this group.
>>>> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
>>>> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
>>>> Group Owner: openembedded-core+owner@lists.openembedded.org
>>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>
SUMMARY = "An extremely fast Python package and project manager, \
written in Rust."
HOMEPAGE = "https://pypi.org/project/uv/"
LICENSE = "Apache-2.0 & BSD-2-Clause & MIT"
LIC_FILES_CHKSUM = "file://LICENSE-APACHE;md5=86d3f3a95c324c9479bd8986968f4327 \
                    file://LICENSE-MIT;md5=45674e482567aa99fe883d3270b11184"
RECIPE_MAINTAINER = "Tom Geelen <t.f.g.geelen@gmail.com>"

SRC_URI[sha256sum] = "8250d61be8ad7da952dff476d9855b2d0b3b5737819cc12ecd94ae6665aab322"

SRCREV_FORMAT = "default"

SRC_URI += "git://github.com/astral-sh/pubgrub;protocol=https;nobranch=1;name=pubgrub;destsuffix=pubgrub"
SRC_URI += "git://github.com/astral-sh/reqwest-middleware;protocol=https;name=reqwest-middleware;destsuffix=reqwest-middleware;branch=main"
SRC_URI += "git://github.com/astral-sh/tl.git;protocol=https;name=tl;destsuffix=tl;branch=master"
SRC_URI += "git://github.com/astral-sh/rs-async-zip;protocol=https;name=rs-async-zip;destsuffix=rs-async-zip;branch=main"

SRCREV_pubgrub = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db"
SRCREV_reqwest-middleware = "7650ed76215a962a96d94a79be71c27bffde7ab2"
SRCREV_tl = "6e25b2ee2513d75385101a8ff9f591ef51f314ec"
SRCREV_rs-async-zip = "285e48742b74ab109887d62e1ae79e7c15fd4878"

inherit pypi python_maturin cargo-update-recipe-crates

require ${BPN}-crates.inc

PYPI_PACKAGE = "uv"

INSANE_SKIP:${PN} += "already-stripped"
# Autogenerated with 'bitbake -c update_crates python3-uv'

# from Cargo.lock
SRC_URI += " \
    crate://crates.io/addr2line/0.24.2 \
    crate://crates.io/adler2/2.0.1 \
    crate://crates.io/aes/0.8.4 \
    crate://crates.io/aho-corasick/1.1.3 \
    crate://crates.io/allocator-api2/0.2.21 \
    crate://crates.io/ambient-id/0.0.5 \
    crate://crates.io/anes/0.1.6 \
    crate://crates.io/anstream/0.6.20 \
    crate://crates.io/anstyle/1.0.11 \
    crate://crates.io/anstyle-parse/0.2.7 \
    crate://crates.io/anstyle-query/1.1.3 \
    crate://crates.io/anstyle-wincon/3.0.9 \
    crate://crates.io/anyhow/1.0.99 \
    crate://crates.io/approx/0.5.1 \
    crate://crates.io/arbitrary/1.4.1 \
    crate://crates.io/arcstr/1.2.0 \
    crate://crates.io/arrayref/0.3.9 \
    crate://crates.io/arrayvec/0.7.6 \
    crate://crates.io/assert-json-diff/2.0.2 \
    crate://crates.io/assert_cmd/2.0.17 \
    crate://crates.io/assert_fs/1.1.3 \
    crate://crates.io/astral-tokio-tar/0.5.3 \
    crate://crates.io/async-broadcast/0.7.2 \
    crate://crates.io/async-channel/2.5.0 \
    crate://crates.io/async-compression/0.4.19 \
    crate://crates.io/async-recursion/1.1.1 \
    crate://crates.io/async-trait/0.1.89 \
    crate://crates.io/async_http_range_reader/0.9.1 \
    crate://crates.io/atomic-waker/1.1.2 \
    crate://crates.io/autocfg/1.5.0 \
    crate://crates.io/axoasset/1.3.0 \
    crate://crates.io/axoprocess/0.2.1 \
    crate://crates.io/axotag/0.3.0 \
    crate://crates.io/axoupdater/0.9.1 \
    crate://crates.io/backon/1.5.2 \
    crate://crates.io/backtrace/0.3.75 \
    crate://crates.io/base64/0.21.7 \
    crate://crates.io/base64/0.22.1 \
    crate://crates.io/bincode/1.3.3 \
    crate://crates.io/bisection/0.1.0 \
    crate://crates.io/bitflags/1.3.2 \
    crate://crates.io/bitflags/2.9.4 \
    crate://crates.io/blake2/0.10.6 \
    crate://crates.io/block-buffer/0.10.4 \
    crate://crates.io/block-padding/0.3.3 \
    crate://crates.io/boxcar/0.2.14 \
    crate://crates.io/bstr/1.12.0 \
    crate://crates.io/bumpalo/3.19.0 \
    crate://crates.io/bytecheck/0.8.1 \
    crate://crates.io/bytecheck_derive/0.8.1 \
    crate://crates.io/bytemuck/1.23.1 \
    crate://crates.io/byteorder/1.5.0 \
    crate://crates.io/byteorder-lite/0.1.0 \
    crate://crates.io/bytes/1.10.1 \
    crate://crates.io/bzip2/0.5.2 \
    crate://crates.io/bzip2-sys/0.1.13+1.0.8 \
    crate://crates.io/camino/1.1.10 \
    crate://crates.io/cargo-util/0.2.22 \
    crate://crates.io/cast/0.3.0 \
    crate://crates.io/cbc/0.1.2 \
    crate://crates.io/cc/1.2.30 \
    crate://crates.io/cfg-if/1.0.1 \
    crate://crates.io/cfg_aliases/0.2.1 \
    crate://crates.io/charset/0.1.5 \
    crate://crates.io/ciborium/0.2.2 \
    crate://crates.io/ciborium-io/0.2.2 \
    crate://crates.io/ciborium-ll/0.2.2 \
    crate://crates.io/cipher/0.4.4 \
    crate://crates.io/clap/4.5.47 \
    crate://crates.io/clap_builder/4.5.47 \
    crate://crates.io/clap_complete/4.5.55 \
    crate://crates.io/clap_complete_command/0.6.1 \
    crate://crates.io/clap_complete_nushell/4.5.8 \
    crate://crates.io/clap_derive/4.5.47 \
    crate://crates.io/clap_lex/0.7.5 \
    crate://crates.io/codspeed/3.0.5 \
    crate://crates.io/codspeed-criterion-compat/3.0.5 \
    crate://crates.io/codspeed-criterion-compat-walltime/3.0.5 \
    crate://crates.io/color_quant/1.1.0 \
    crate://crates.io/colorchoice/1.0.4 \
    crate://crates.io/colored/2.2.0 \
    crate://crates.io/concurrent-queue/2.5.0 \
    crate://crates.io/configparser/3.1.0 \
    crate://crates.io/console/0.15.11 \
    crate://crates.io/console/0.16.1 \
    crate://crates.io/core-foundation/0.9.4 \
    crate://crates.io/core-foundation/0.10.1 \
    crate://crates.io/core-foundation-sys/0.8.7 \
    crate://crates.io/cpufeatures/0.2.17 \
    crate://crates.io/crc/3.3.0 \
    crate://crates.io/crc-catalog/2.4.0 \
    crate://crates.io/crc32fast/1.5.0 \
    crate://crates.io/criterion/0.7.0 \
    crate://crates.io/criterion-plot/0.5.0 \
    crate://crates.io/criterion-plot/0.6.0 \
    crate://crates.io/crossbeam-deque/0.8.6 \
    crate://crates.io/crossbeam-epoch/0.9.18 \
    crate://crates.io/crossbeam-utils/0.8.21 \
    crate://crates.io/crunchy/0.2.4 \
    crate://crates.io/crypto-common/0.1.6 \
    crate://crates.io/csv/1.3.1 \
    crate://crates.io/csv-core/0.1.12 \
    crate://crates.io/ctrlc/3.5.0 \
    crate://crates.io/dashmap/6.1.0 \
    crate://crates.io/data-encoding/2.9.0 \
    crate://crates.io/data-url/0.2.0 \
    crate://crates.io/deadpool/0.12.3 \
    crate://crates.io/deadpool-runtime/0.1.4 \
    crate://crates.io/derive_arbitrary/1.4.1 \
    crate://crates.io/diff/0.1.13 \
    crate://crates.io/difflib/0.4.0 \
    crate://crates.io/digest/0.10.7 \
    crate://crates.io/dirs/6.0.0 \
    crate://crates.io/dirs-sys/0.5.0 \
    crate://crates.io/dispatch/0.2.0 \
    crate://crates.io/displaydoc/0.2.5 \
    crate://crates.io/doc-comment/0.3.3 \
    crate://crates.io/dotenvy/0.15.7 \
    crate://crates.io/dunce/1.0.5 \
    crate://crates.io/dyn-clone/1.0.19 \
    crate://crates.io/either/1.15.0 \
    crate://crates.io/encode_unicode/1.0.0 \
    crate://crates.io/encoding_rs/0.8.35 \
    crate://crates.io/encoding_rs_io/0.1.7 \
    crate://crates.io/endi/1.1.0 \
    crate://crates.io/enumflags2/0.7.12 \
    crate://crates.io/enumflags2_derive/0.7.12 \
    crate://crates.io/env_filter/0.1.3 \
    crate://crates.io/env_home/0.1.0 \
    crate://crates.io/env_logger/0.11.8 \
    crate://crates.io/equivalent/1.0.2 \
    crate://crates.io/erased-serde/0.4.6 \
    crate://crates.io/errno/0.3.13 \
    crate://crates.io/etcetera/0.10.0 \
    crate://crates.io/event-listener/5.4.0 \
    crate://crates.io/event-listener-strategy/0.5.4 \
    crate://crates.io/fastrand/2.3.0 \
    crate://crates.io/fdeflate/0.3.7 \
    crate://crates.io/filetime/0.2.26 \
    crate://crates.io/fixedbitset/0.5.7 \
    crate://crates.io/flate2/1.1.2 \
    crate://crates.io/float-cmp/0.9.0 \
    crate://crates.io/float-cmp/0.10.0 \
    crate://crates.io/fnv/1.0.7 \
    crate://crates.io/foldhash/0.1.5 \
    crate://crates.io/foldhash/0.2.0 \
    crate://crates.io/fontconfig-parser/0.5.8 \
    crate://crates.io/fontdb/0.12.0 \
    crate://crates.io/form_urlencoded/1.2.2 \
    crate://crates.io/fs-err/3.1.1 \
    crate://crates.io/fs2/0.4.3 \
    crate://crates.io/futures/0.3.31 \
    crate://crates.io/futures-channel/0.3.31 \
    crate://crates.io/futures-core/0.3.31 \
    crate://crates.io/futures-executor/0.3.31 \
    crate://crates.io/futures-io/0.3.31 \
    crate://crates.io/futures-lite/2.6.0 \
    crate://crates.io/futures-macro/0.3.31 \
    crate://crates.io/futures-sink/0.3.31 \
    crate://crates.io/futures-task/0.3.31 \
    crate://crates.io/futures-util/0.3.31 \
    crate://crates.io/generic-array/0.14.7 \
    crate://crates.io/getrandom/0.2.16 \
    crate://crates.io/getrandom/0.3.3 \
    crate://crates.io/gif/0.12.0 \
    crate://crates.io/gimli/0.31.1 \
    crate://crates.io/glob/0.3.3 \
    crate://crates.io/globset/0.4.16 \
    crate://crates.io/globwalk/0.9.1 \
    crate://crates.io/gloo-timers/0.3.0 \
    crate://crates.io/goblin/0.10.1 \
    crate://crates.io/h2/0.4.12 \
    crate://crates.io/half/2.6.0 \
    crate://crates.io/hashbrown/0.14.5 \
    crate://crates.io/hashbrown/0.15.5 \
    crate://crates.io/hashbrown/0.16.0 \
    crate://crates.io/heck/0.5.0 \
    crate://crates.io/hermit-abi/0.5.2 \
    crate://crates.io/hex/0.4.3 \
    crate://crates.io/hkdf/0.12.4 \
    crate://crates.io/hmac/0.12.1 \
    crate://crates.io/home/0.5.11 \
    crate://crates.io/homedir/0.3.6 \
    crate://crates.io/html-escape/0.2.13 \
    crate://crates.io/http/1.3.1 \
    crate://crates.io/http-body/1.0.1 \
    crate://crates.io/http-body-util/0.1.3 \
    crate://crates.io/http-content-range/0.2.3 \
    crate://crates.io/httparse/1.10.1 \
    crate://crates.io/httpdate/1.0.3 \
    crate://crates.io/hyper/1.7.0 \
    crate://crates.io/hyper-rustls/0.27.7 \
    crate://crates.io/hyper-util/0.1.16 \
    crate://crates.io/icu_collections/2.0.0 \
    crate://crates.io/icu_locale_core/2.0.0 \
    crate://crates.io/icu_normalizer/2.0.0 \
    crate://crates.io/icu_normalizer_data/2.0.0 \
    crate://crates.io/icu_properties/2.0.1 \
    crate://crates.io/icu_properties_data/2.0.1 \
    crate://crates.io/icu_provider/2.0.0 \
    crate://crates.io/idna/1.1.0 \
    crate://crates.io/idna_adapter/1.2.1 \
    crate://crates.io/ignore/0.4.23 \
    crate://crates.io/image/0.25.6 \
    crate://crates.io/imagesize/0.11.0 \
    crate://crates.io/indexmap/2.10.0 \
    crate://crates.io/indicatif/0.18.0 \
    crate://crates.io/indoc/2.0.6 \
    crate://crates.io/inout/0.1.4 \
    crate://crates.io/insta/1.43.2 \
    crate://crates.io/io-uring/0.7.9 \
    crate://crates.io/ipnet/2.11.0 \
    crate://crates.io/iri-string/0.7.8 \
    crate://crates.io/is-docker/0.2.0 \
    crate://crates.io/is-terminal/0.4.16 \
    crate://crates.io/is-wsl/0.4.0 \
    crate://crates.io/is_ci/1.2.0 \
    crate://crates.io/is_terminal_polyfill/1.70.1 \
    crate://crates.io/itertools/0.10.5 \
    crate://crates.io/itertools/0.13.0 \
    crate://crates.io/itertools/0.14.0 \
    crate://crates.io/itoa/1.0.15 \
    crate://crates.io/jiff/0.2.15 \
    crate://crates.io/jiff-static/0.2.15 \
    crate://crates.io/jiff-tzdb/0.1.4 \
    crate://crates.io/jiff-tzdb-platform/0.1.3 \
    crate://crates.io/jobserver/0.1.33 \
    crate://crates.io/jpeg-decoder/0.3.2 \
    crate://crates.io/js-sys/0.3.77 \
    crate://crates.io/junction/1.2.0 \
    crate://crates.io/kurbo/0.8.3 \
    crate://crates.io/kurbo/0.9.5 \
    crate://crates.io/lazy_static/1.5.0 \
    crate://crates.io/libc/0.2.175 \
    crate://crates.io/libmimalloc-sys/0.1.43 \
    crate://crates.io/libredox/0.1.6 \
    crate://crates.io/libz-rs-sys/0.5.1 \
    crate://crates.io/linux-raw-sys/0.4.15 \
    crate://crates.io/linux-raw-sys/0.9.4 \
    crate://crates.io/litemap/0.8.0 \
    crate://crates.io/lock_api/0.4.13 \
    crate://crates.io/log/0.4.27 \
    crate://crates.io/lru-slab/0.1.2 \
    crate://crates.io/lzma-rs/0.3.0 \
    crate://crates.io/lzma-sys/0.1.20 \
    crate://crates.io/mailparse/0.16.1 \
    crate://crates.io/markdown/1.0.0 \
    crate://crates.io/matchers/0.2.0 \
    crate://crates.io/md-5/0.10.6 \
    crate://crates.io/memchr/2.7.5 \
    crate://crates.io/memmap2/0.5.10 \
    crate://crates.io/memmap2/0.9.7 \
    crate://crates.io/memoffset/0.9.1 \
    crate://crates.io/miette/7.6.0 \
    crate://crates.io/miette-derive/7.6.0 \
    crate://crates.io/mimalloc/0.1.47 \
    crate://crates.io/mime/0.3.17 \
    crate://crates.io/mime_guess/2.0.5 \
    crate://crates.io/miniz_oxide/0.8.9 \
    crate://crates.io/mio/1.0.4 \
    crate://crates.io/miow/0.6.1 \
    crate://crates.io/munge/0.4.5 \
    crate://crates.io/munge_macro/0.4.5 \
    crate://crates.io/nanoid/0.4.0 \
    crate://crates.io/nix/0.29.0 \
    crate://crates.io/nix/0.30.1 \
    crate://crates.io/normalize-line-endings/0.3.0 \
    crate://crates.io/nu-ansi-term/0.50.1 \
    crate://crates.io/num/0.4.3 \
    crate://crates.io/num-bigint/0.4.6 \
    crate://crates.io/num-complex/0.4.6 \
    crate://crates.io/num-integer/0.1.46 \
    crate://crates.io/num-iter/0.1.45 \
    crate://crates.io/num-rational/0.4.2 \
    crate://crates.io/num-traits/0.2.19 \
    crate://crates.io/num_cpus/1.17.0 \
    crate://crates.io/object/0.36.7 \
    crate://crates.io/once_cell/1.21.3 \
    crate://crates.io/once_cell_polyfill/1.70.1 \
    crate://crates.io/oorandom/11.1.5 \
    crate://crates.io/open/5.3.2 \
    crate://crates.io/openssl-probe/0.1.6 \
    crate://crates.io/option-ext/0.2.0 \
    crate://crates.io/ordered-stream/0.2.0 \
    crate://crates.io/os_str_bytes/6.6.1 \
    crate://crates.io/owo-colors/4.2.2 \
    crate://crates.io/parking/2.2.1 \
    crate://crates.io/parking_lot/0.12.4 \
    crate://crates.io/parking_lot_core/0.9.11 \
    crate://crates.io/paste/1.0.15 \
    crate://crates.io/path-slash/0.2.1 \
    crate://crates.io/pathdiff/0.2.3 \
    crate://crates.io/percent-encoding/2.3.2 \
    crate://crates.io/pest/2.8.1 \
    crate://crates.io/pest_derive/2.8.1 \
    crate://crates.io/pest_generator/2.8.1 \
    crate://crates.io/pest_meta/2.8.1 \
    crate://crates.io/petgraph/0.8.2 \
    crate://crates.io/pico-args/0.5.0 \
    crate://crates.io/pin-project/1.1.10 \
    crate://crates.io/pin-project-internal/1.1.10 \
    crate://crates.io/pin-project-lite/0.2.16 \
    crate://crates.io/pin-utils/0.1.0 \
    crate://crates.io/pkg-config/0.3.32 \
    crate://crates.io/plain/0.2.3 \
    crate://crates.io/png/0.17.16 \
    crate://crates.io/poloto/19.1.2 \
    crate://crates.io/portable-atomic/1.11.1 \
    crate://crates.io/portable-atomic-util/0.2.4 \
    crate://crates.io/potential_utf/0.1.2 \
    crate://crates.io/ppv-lite86/0.2.21 \
    crate://crates.io/predicates/3.1.3 \
    crate://crates.io/predicates-core/1.0.9 \
    crate://crates.io/predicates-tree/1.0.12 \
    crate://crates.io/pretty_assertions/1.4.1 \
    crate://crates.io/priority-queue/2.5.0 \
    crate://crates.io/proc-macro-crate/3.3.0 \
    crate://crates.io/proc-macro2/1.0.101 \
    crate://crates.io/procfs/0.17.0 \
    crate://crates.io/procfs-core/0.17.0 \
    crate://crates.io/ptr_meta/0.3.0 \
    crate://crates.io/ptr_meta_derive/0.3.0 \
    crate://crates.io/quinn/0.11.8 \
    crate://crates.io/quinn-proto/0.11.12 \
    crate://crates.io/quinn-udp/0.5.13 \
    crate://crates.io/quote/1.0.40 \
    crate://crates.io/quoted_printable/0.5.1 \
    crate://crates.io/r-efi/5.3.0 \
    crate://crates.io/rancor/0.1.0 \
    crate://crates.io/rand/0.8.5 \
    crate://crates.io/rand/0.9.2 \
    crate://crates.io/rand_chacha/0.3.1 \
    crate://crates.io/rand_chacha/0.9.0 \
    crate://crates.io/rand_core/0.6.4 \
    crate://crates.io/rand_core/0.9.3 \
    crate://crates.io/rayon/1.10.0 \
    crate://crates.io/rayon-core/1.12.1 \
    crate://crates.io/rctree/0.5.0 \
    crate://crates.io/redox_syscall/0.5.15 \
    crate://crates.io/redox_users/0.5.0 \
    crate://crates.io/ref-cast/1.0.24 \
    crate://crates.io/ref-cast-impl/1.0.24 \
    crate://crates.io/reflink-copy/0.1.28 \
    crate://crates.io/regex/1.11.2 \
    crate://crates.io/regex-automata/0.4.10 \
    crate://crates.io/regex-syntax/0.8.5 \
    crate://crates.io/rend/0.5.2 \
    crate://crates.io/reqwest/0.12.22 \
    crate://crates.io/resvg/0.29.0 \
    crate://crates.io/retry-policies/0.4.0 \
    crate://crates.io/rgb/0.8.52 \
    crate://crates.io/ring/0.17.14 \
    crate://crates.io/rkyv/0.8.11 \
    crate://crates.io/rkyv_derive/0.8.11 \
    crate://crates.io/rmp/0.8.14 \
    crate://crates.io/rmp-serde/1.3.0 \
    crate://crates.io/rosvgtree/0.1.0 \
    crate://crates.io/roxmltree/0.18.1 \
    crate://crates.io/roxmltree/0.20.0 \
    crate://crates.io/rust-netrc/0.1.2 \
    crate://crates.io/rustc-demangle/0.1.25 \
    crate://crates.io/rustc-hash/2.1.1 \
    crate://crates.io/rustix/0.38.44 \
    crate://crates.io/rustix/1.0.8 \
    crate://crates.io/rustls/0.23.29 \
    crate://crates.io/rustls-native-certs/0.8.1 \
    crate://crates.io/rustls-pki-types/1.12.0 \
    crate://crates.io/rustls-webpki/0.103.4 \
    crate://crates.io/rustversion/1.0.21 \
    crate://crates.io/rustybuzz/0.7.0 \
    crate://crates.io/ryu/1.0.20 \
    crate://crates.io/same-file/1.0.6 \
    crate://crates.io/schannel/0.1.27 \
    crate://crates.io/schemars/1.0.4 \
    crate://crates.io/schemars_derive/1.0.4 \
    crate://crates.io/scopeguard/1.2.0 \
    crate://crates.io/scroll/0.13.0 \
    crate://crates.io/scroll_derive/0.13.0 \
    crate://crates.io/seahash/4.1.0 \
    crate://crates.io/secrecy/0.10.3 \
    crate://crates.io/secret-service/5.0.0 \
    crate://crates.io/security-framework/3.2.0 \
    crate://crates.io/security-framework-sys/2.14.0 \
    crate://crates.io/self-replace/1.5.0 \
    crate://crates.io/semver/1.0.26 \
    crate://crates.io/serde/1.0.223 \
    crate://crates.io/serde-untagged/0.1.9 \
    crate://crates.io/serde_core/1.0.223 \
    crate://crates.io/serde_derive/1.0.223 \
    crate://crates.io/serde_derive_internals/0.29.1 \
    crate://crates.io/serde_json/1.0.145 \
    crate://crates.io/serde_repr/0.1.20 \
    crate://crates.io/serde_spanned/1.0.0 \
    crate://crates.io/serde_urlencoded/0.7.1 \
    crate://crates.io/serde_yaml/0.9.34+deprecated \
    crate://crates.io/sha2/0.10.9 \
    crate://crates.io/sharded-slab/0.1.7 \
    crate://crates.io/shell-escape/0.1.5 \
    crate://crates.io/shellexpand/3.1.1 \
    crate://crates.io/shlex/1.3.0 \
    crate://crates.io/signal-hook-registry/1.4.5 \
    crate://crates.io/simd-adler32/0.3.7 \
    crate://crates.io/simdutf8/0.1.5 \
    crate://crates.io/similar/2.7.0 \
    crate://crates.io/simplecss/0.2.2 \
    crate://crates.io/siphasher/0.3.11 \
    crate://crates.io/slab/0.4.10 \
    crate://crates.io/smallvec/1.15.1 \
    crate://crates.io/smawk/0.3.2 \
    crate://crates.io/socket2/0.5.10 \
    crate://crates.io/socket2/0.6.0 \
    crate://crates.io/spdx/0.10.9 \
    crate://crates.io/stable_deref_trait/1.2.0 \
    crate://crates.io/static_assertions/1.1.0 \
    crate://crates.io/statrs/0.18.0 \
    crate://crates.io/strict-num/0.1.1 \
    crate://crates.io/strsim/0.11.1 \
    crate://crates.io/subtle/2.6.1 \
    crate://crates.io/supports-color/3.0.2 \
    crate://crates.io/supports-hyperlinks/3.1.0 \
    crate://crates.io/supports-unicode/3.0.0 \
    crate://crates.io/svg/0.18.0 \
    crate://crates.io/svgfilters/0.4.0 \
    crate://crates.io/svgtypes/0.9.0 \
    crate://crates.io/svgtypes/0.10.0 \
    crate://crates.io/syn/2.0.106 \
    crate://crates.io/sync_wrapper/1.0.2 \
    crate://crates.io/synstructure/0.13.2 \
    crate://crates.io/sys-info/0.9.1 \
    crate://crates.io/system-configuration/0.6.1 \
    crate://crates.io/system-configuration-sys/0.6.0 \
    crate://crates.io/tagu/0.1.6 \
    crate://crates.io/tar/0.4.44 \
    crate://crates.io/target-lexicon/0.13.3 \
    crate://crates.io/temp-env/0.3.6 \
    crate://crates.io/tempfile/3.20.0 \
    crate://crates.io/terminal_size/0.4.2 \
    crate://crates.io/termtree/0.5.1 \
    crate://crates.io/test-case/3.3.1 \
    crate://crates.io/test-case-core/3.3.1 \
    crate://crates.io/test-case-macros/3.3.1 \
    crate://crates.io/test-log/0.2.18 \
    crate://crates.io/test-log-macros/0.2.18 \
    crate://crates.io/textwrap/0.16.2 \
    crate://crates.io/thiserror/1.0.69 \
    crate://crates.io/thiserror/2.0.16 \
    crate://crates.io/thiserror-impl/1.0.69 \
    crate://crates.io/thiserror-impl/2.0.16 \
    crate://crates.io/thread_local/1.1.9 \
    crate://crates.io/tikv-jemalloc-sys/0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7 \
    crate://crates.io/tikv-jemallocator/0.6.0 \
    crate://crates.io/tiny-skia/0.8.4 \
    crate://crates.io/tiny-skia-path/0.8.4 \
    crate://crates.io/tinystr/0.8.1 \
    crate://crates.io/tinytemplate/1.2.1 \
    crate://crates.io/tinyvec/1.9.0 \
    crate://crates.io/tinyvec_macros/0.1.1 \
    crate://crates.io/tokio/1.47.1 \
    crate://crates.io/tokio-macros/2.5.0 \
    crate://crates.io/tokio-rustls/0.26.2 \
    crate://crates.io/tokio-stream/0.1.17 \
    crate://crates.io/tokio-util/0.7.15 \
    crate://crates.io/toml/0.9.5 \
    crate://crates.io/toml_datetime/0.6.11 \
    crate://crates.io/toml_datetime/0.7.0 \
    crate://crates.io/toml_edit/0.22.27 \
    crate://crates.io/toml_edit/0.23.4 \
    crate://crates.io/toml_parser/1.0.2 \
    crate://crates.io/toml_writer/1.0.2 \
    crate://crates.io/tower/0.5.2 \
    crate://crates.io/tower-http/0.6.6 \
    crate://crates.io/tower-layer/0.3.3 \
    crate://crates.io/tower-service/0.3.3 \
    crate://crates.io/tracing/0.1.41 \
    crate://crates.io/tracing-attributes/0.1.30 \
    crate://crates.io/tracing-core/0.1.34 \
    crate://crates.io/tracing-durations-export/0.3.1 \
    crate://crates.io/tracing-log/0.2.0 \
    crate://crates.io/tracing-serde/0.2.0 \
    crate://crates.io/tracing-subscriber/0.3.20 \
    crate://crates.io/tracing-test/0.2.5 \
    crate://crates.io/tracing-test-macro/0.2.5 \
    crate://crates.io/tracing-tree/0.4.0 \
    crate://crates.io/try-lock/0.2.5 \
    crate://crates.io/ttf-parser/0.18.1 \
    crate://crates.io/typeid/1.0.3 \
    crate://crates.io/typenum/1.18.0 \
    crate://crates.io/ucd-trie/0.1.7 \
    crate://crates.io/uds_windows/1.1.0 \
    crate://crates.io/unicase/2.8.1 \
    crate://crates.io/unicode-bidi/0.3.18 \
    crate://crates.io/unicode-bidi-mirroring/0.1.0 \
    crate://crates.io/unicode-ccc/0.1.2 \
    crate://crates.io/unicode-general-category/0.6.0 \
    crate://crates.io/unicode-id/0.3.5 \
    crate://crates.io/unicode-ident/1.0.18 \
    crate://crates.io/unicode-linebreak/0.1.5 \
    crate://crates.io/unicode-script/0.5.7 \
    crate://crates.io/unicode-vo/0.1.0 \
    crate://crates.io/unicode-width/0.1.14 \
    crate://crates.io/unicode-width/0.2.1 \
    crate://crates.io/unit-prefix/0.5.1 \
    crate://crates.io/unsafe-libyaml/0.2.11 \
    crate://crates.io/unscanny/0.1.0 \
    crate://crates.io/untrusted/0.9.0 \
    crate://crates.io/url/2.5.7 \
    crate://crates.io/usvg/0.29.0 \
    crate://crates.io/usvg-text-layout/0.29.0 \
    crate://crates.io/utf8-width/0.1.7 \
    crate://crates.io/utf8_iter/1.0.4 \
    crate://crates.io/utf8parse/0.2.2 \
    crate://crates.io/uuid/1.17.0 \
    crate://crates.io/valuable/0.1.1 \
    crate://crates.io/version_check/0.9.5 \
    crate://crates.io/wait-timeout/0.2.1 \
    crate://crates.io/walkdir/2.5.0 \
    crate://crates.io/want/0.3.1 \
    crate://crates.io/wasi/0.11.1+wasi-snapshot-preview1 \
    crate://crates.io/wasi/0.14.2+wasi-0.2.4 \
    crate://crates.io/wasite/0.1.0 \
    crate://crates.io/wasm-bindgen/0.2.100 \
    crate://crates.io/wasm-bindgen-backend/0.2.100 \
    crate://crates.io/wasm-bindgen-futures/0.4.50 \
    crate://crates.io/wasm-bindgen-macro/0.2.100 \
    crate://crates.io/wasm-bindgen-macro-support/0.2.100 \
    crate://crates.io/wasm-bindgen-shared/0.2.100 \
    crate://crates.io/wasm-streams/0.4.2 \
    crate://crates.io/wasmtimer/0.4.2 \
    crate://crates.io/web-sys/0.3.77 \
    crate://crates.io/web-time/1.1.0 \
    crate://crates.io/webpki-roots/1.0.2 \
    crate://crates.io/weezl/0.1.10 \
    crate://crates.io/which/8.0.0 \
    crate://crates.io/whoami/1.6.1 \
    crate://crates.io/widestring/1.2.0 \
    crate://crates.io/winapi/0.3.9 \
    crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \
    crate://crates.io/winapi-util/0.1.9 \
    crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \
    crate://crates.io/windows/0.59.0 \
    crate://crates.io/windows/0.61.3 \
    crate://crates.io/windows-collections/0.2.0 \
    crate://crates.io/windows-core/0.59.0 \
    crate://crates.io/windows-core/0.61.2 \
    crate://crates.io/windows-future/0.2.1 \
    crate://crates.io/windows-implement/0.59.0 \
    crate://crates.io/windows-implement/0.60.0 \
    crate://crates.io/windows-interface/0.59.1 \
    crate://crates.io/windows-link/0.1.3 \
    crate://crates.io/windows-link/0.2.0 \
    crate://crates.io/windows-numerics/0.2.0 \
    crate://crates.io/windows-registry/0.5.3 \
    crate://crates.io/windows-result/0.3.4 \
    crate://crates.io/windows-strings/0.3.1 \
    crate://crates.io/windows-strings/0.4.2 \
    crate://crates.io/windows-sys/0.52.0 \
    crate://crates.io/windows-sys/0.59.0 \
    crate://crates.io/windows-sys/0.60.2 \
    crate://crates.io/windows-sys/0.61.0 \
    crate://crates.io/windows-targets/0.52.6 \
    crate://crates.io/windows-targets/0.53.2 \
    crate://crates.io/windows-threading/0.1.0 \
    crate://crates.io/windows_aarch64_gnullvm/0.52.6 \
    crate://crates.io/windows_aarch64_gnullvm/0.53.0 \
    crate://crates.io/windows_aarch64_msvc/0.52.6 \
    crate://crates.io/windows_aarch64_msvc/0.53.0 \
    crate://crates.io/windows_i686_gnu/0.52.6 \
    crate://crates.io/windows_i686_gnu/0.53.0 \
    crate://crates.io/windows_i686_gnullvm/0.52.6 \
    crate://crates.io/windows_i686_gnullvm/0.53.0 \
    crate://crates.io/windows_i686_msvc/0.52.6 \
    crate://crates.io/windows_i686_msvc/0.53.0 \
    crate://crates.io/windows_x86_64_gnu/0.52.6 \
    crate://crates.io/windows_x86_64_gnu/0.53.0 \
    crate://crates.io/windows_x86_64_gnullvm/0.52.6 \
    crate://crates.io/windows_x86_64_gnullvm/0.53.0 \
    crate://crates.io/windows_x86_64_msvc/0.52.6 \
    crate://crates.io/windows_x86_64_msvc/0.53.0 \
    crate://crates.io/winnow/0.7.12 \
    crate://crates.io/winsafe/0.0.19 \
    crate://crates.io/wiremock/0.6.5 \
    crate://crates.io/wit-bindgen-rt/0.39.0 \
    crate://crates.io/writeable/0.6.1 \
    crate://crates.io/xattr/1.5.1 \
    crate://crates.io/xmlparser/0.13.6 \
    crate://crates.io/xz2/0.1.7 \
    crate://crates.io/yansi/1.0.1 \
    crate://crates.io/yoke/0.8.0 \
    crate://crates.io/yoke-derive/0.8.0 \
    crate://crates.io/zbus/5.8.0 \
    crate://crates.io/zbus_macros/5.8.0 \
    crate://crates.io/zbus_names/4.2.0 \
    crate://crates.io/zerocopy/0.8.26 \
    crate://crates.io/zerocopy-derive/0.8.26 \
    crate://crates.io/zerofrom/0.1.6 \
    crate://crates.io/zerofrom-derive/0.1.6 \
    crate://crates.io/zeroize/1.8.1 \
    crate://crates.io/zerotrie/0.2.2 \
    crate://crates.io/zerovec/0.11.2 \
    crate://crates.io/zerovec-derive/0.11.1 \
    crate://crates.io/zip/2.4.2 \
    crate://crates.io/zlib-rs/0.5.1 \
    crate://crates.io/zopfli/0.8.2 \
    crate://crates.io/zstd/0.13.3 \
    crate://crates.io/zstd-safe/7.2.4 \
    crate://crates.io/zstd-sys/2.0.15+zstd.1.5.7 \
    crate://crates.io/zvariant/5.6.0 \
    crate://crates.io/zvariant_derive/5.6.0 \
    crate://crates.io/zvariant_utils/3.2.0 \
"

SRC_URI[addr2line-0.24.2.sha256sum] = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
SRC_URI[adler2-2.0.1.sha256sum] = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
SRC_URI[aes-0.8.4.sha256sum] = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
SRC_URI[aho-corasick-1.1.3.sha256sum] = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
SRC_URI[allocator-api2-0.2.21.sha256sum] = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
SRC_URI[ambient-id-0.0.5.sha256sum] = "a55e62faa820045efacb144fd9bcb16e62a5960ffc4bc270aaff7b78f0fcdcaa"
SRC_URI[anes-0.1.6.sha256sum] = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
SRC_URI[anstream-0.6.20.sha256sum] = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
SRC_URI[anstyle-1.0.11.sha256sum] = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
SRC_URI[anstyle-parse-0.2.7.sha256sum] = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
SRC_URI[anstyle-query-1.1.3.sha256sum] = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
SRC_URI[anstyle-wincon-3.0.9.sha256sum] = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
SRC_URI[anyhow-1.0.99.sha256sum] = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
SRC_URI[approx-0.5.1.sha256sum] = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
SRC_URI[arbitrary-1.4.1.sha256sum] = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
SRC_URI[arcstr-1.2.0.sha256sum] = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d"
SRC_URI[arrayref-0.3.9.sha256sum] = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
SRC_URI[arrayvec-0.7.6.sha256sum] = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
SRC_URI[assert-json-diff-2.0.2.sha256sum] = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
SRC_URI[assert_cmd-2.0.17.sha256sum] = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
SRC_URI[assert_fs-1.1.3.sha256sum] = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9"
SRC_URI[astral-tokio-tar-0.5.3.sha256sum] = "0036af73142caf1291d4ec8ed667d3a1145bd55c8189517bd5aa07b3167ae1e1"
SRC_URI[async-broadcast-0.7.2.sha256sum] = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
SRC_URI[async-channel-2.5.0.sha256sum] = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
SRC_URI[async-compression-0.4.19.sha256sum] = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c"
SRC_URI[async-recursion-1.1.1.sha256sum] = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
SRC_URI[async-trait-0.1.89.sha256sum] = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
SRC_URI[async_http_range_reader-0.9.1.sha256sum] = "2b537c00269e3f943e06f5d7cabf8ccd281b800fd0c7f111dd82f77154334197"
SRC_URI[atomic-waker-1.1.2.sha256sum] = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
SRC_URI[autocfg-1.5.0.sha256sum] = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
SRC_URI[axoasset-1.3.0.sha256sum] = "56b3b6c5d71b918c0f42f43f69b303d7529b4233a598d9d61759d75f0f2a44a2"
SRC_URI[axoprocess-0.2.1.sha256sum] = "8a4b4798a6c02e91378537c63cd6e91726900b595450daa5d487bc3c11e95e1b"
SRC_URI[axotag-0.3.0.sha256sum] = "dc923121fbc4cc72e9008436b5650b98e56f94b5799df59a1b4f572b5c6a7e6b"
SRC_URI[axoupdater-0.9.1.sha256sum] = "dc482a1926df098f4e3806b834f3fe73a1ab54b24ab0ac481f72de479af5e982"
SRC_URI[backon-1.5.2.sha256sum] = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d"
SRC_URI[backtrace-0.3.75.sha256sum] = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
SRC_URI[base64-0.21.7.sha256sum] = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
SRC_URI[base64-0.22.1.sha256sum] = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
SRC_URI[bincode-1.3.3.sha256sum] = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
SRC_URI[bisection-0.1.0.sha256sum] = "021e079a1bab0ecce6cf4b4b74c0c37afa4a697136eb3b127875c84a8f04a8c3"
SRC_URI[bitflags-1.3.2.sha256sum] = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
SRC_URI[bitflags-2.9.4.sha256sum] = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
SRC_URI[blake2-0.10.6.sha256sum] = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
SRC_URI[block-buffer-0.10.4.sha256sum] = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
SRC_URI[block-padding-0.3.3.sha256sum] = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
SRC_URI[boxcar-0.2.14.sha256sum] = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
SRC_URI[bstr-1.12.0.sha256sum] = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
SRC_URI[bumpalo-3.19.0.sha256sum] = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
SRC_URI[bytecheck-0.8.1.sha256sum] = "50690fb3370fb9fe3550372746084c46f2ac8c9685c583d2be10eefd89d3d1a3"
SRC_URI[bytecheck_derive-0.8.1.sha256sum] = "efb7846e0cb180355c2dec69e721edafa36919850f1a9f52ffba4ebc0393cb71"
SRC_URI[bytemuck-1.23.1.sha256sum] = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
SRC_URI[byteorder-1.5.0.sha256sum] = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
SRC_URI[byteorder-lite-0.1.0.sha256sum] = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
SRC_URI[bytes-1.10.1.sha256sum] = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
SRC_URI[bzip2-0.5.2.sha256sum] = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
SRC_URI[bzip2-sys-0.1.13+1.0.8.sha256sum] = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
SRC_URI[camino-1.1.10.sha256sum] = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
SRC_URI[cargo-util-0.2.22.sha256sum] = "4f46ba11692cd1e4b09cd123877e02b74e180acae237caf905ef20b42e14e206"
SRC_URI[cast-0.3.0.sha256sum] = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
SRC_URI[cbc-0.1.2.sha256sum] = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
SRC_URI[cc-1.2.30.sha256sum] = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
SRC_URI[cfg-if-1.0.1.sha256sum] = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
SRC_URI[cfg_aliases-0.2.1.sha256sum] = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
SRC_URI[charset-0.1.5.sha256sum] = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
SRC_URI[ciborium-0.2.2.sha256sum] = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
SRC_URI[ciborium-io-0.2.2.sha256sum] = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
SRC_URI[ciborium-ll-0.2.2.sha256sum] = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
SRC_URI[cipher-0.4.4.sha256sum] = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
SRC_URI[clap-4.5.47.sha256sum] = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
SRC_URI[clap_builder-4.5.47.sha256sum] = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
SRC_URI[clap_complete-4.5.55.sha256sum] = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
SRC_URI[clap_complete_command-0.6.1.sha256sum] = "da8e198c052315686d36371e8a3c5778b7852fc75cc313e4e11eeb7a644a1b62"
SRC_URI[clap_complete_nushell-4.5.8.sha256sum] = "0a0c951694691e65bf9d421d597d68416c22de9632e884c28412cb8cd8b73dce"
SRC_URI[clap_derive-4.5.47.sha256sum] = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
SRC_URI[clap_lex-0.7.5.sha256sum] = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
SRC_URI[codspeed-3.0.5.sha256sum] = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117"
SRC_URI[codspeed-criterion-compat-3.0.5.sha256sum] = "78f6c1c6bed5fd84d319e8b0889da051daa361c79b7709c9394dfe1a882bba67"
SRC_URI[codspeed-criterion-compat-walltime-3.0.5.sha256sum] = "c989289ce6b1cbde72ed560496cb8fbf5aa14d5ef5666f168e7f87751038352e"
SRC_URI[color_quant-1.1.0.sha256sum] = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
SRC_URI[colorchoice-1.0.4.sha256sum] = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
SRC_URI[colored-2.2.0.sha256sum] = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
SRC_URI[concurrent-queue-2.5.0.sha256sum] = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
SRC_URI[configparser-3.1.0.sha256sum] = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
SRC_URI[console-0.15.11.sha256sum] = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
SRC_URI[console-0.16.1.sha256sum] = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
SRC_URI[core-foundation-0.9.4.sha256sum] = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
SRC_URI[core-foundation-0.10.1.sha256sum] = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
SRC_URI[core-foundation-sys-0.8.7.sha256sum] = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
SRC_URI[cpufeatures-0.2.17.sha256sum] = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
SRC_URI[crc-3.3.0.sha256sum] = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
SRC_URI[crc-catalog-2.4.0.sha256sum] = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
SRC_URI[crc32fast-1.5.0.sha256sum] = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
SRC_URI[criterion-0.7.0.sha256sum] = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
SRC_URI[criterion-plot-0.5.0.sha256sum] = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
SRC_URI[criterion-plot-0.6.0.sha256sum] = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
SRC_URI[crossbeam-deque-0.8.6.sha256sum] = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
SRC_URI[crossbeam-epoch-0.9.18.sha256sum] = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
SRC_URI[crossbeam-utils-0.8.21.sha256sum] = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
SRC_URI[crunchy-0.2.4.sha256sum] = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
SRC_URI[crypto-common-0.1.6.sha256sum] = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
SRC_URI[csv-1.3.1.sha256sum] = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
SRC_URI[csv-core-0.1.12.sha256sum] = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
SRC_URI[ctrlc-3.5.0.sha256sum] = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3"
SRC_URI[dashmap-6.1.0.sha256sum] = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
SRC_URI[data-encoding-2.9.0.sha256sum] = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
SRC_URI[data-url-0.2.0.sha256sum] = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
SRC_URI[deadpool-0.12.3.sha256sum] = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
SRC_URI[deadpool-runtime-0.1.4.sha256sum] = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
SRC_URI[derive_arbitrary-1.4.1.sha256sum] = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
SRC_URI[diff-0.1.13.sha256sum] = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
SRC_URI[difflib-0.4.0.sha256sum] = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
SRC_URI[digest-0.10.7.sha256sum] = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
SRC_URI[dirs-6.0.0.sha256sum] = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
SRC_URI[dirs-sys-0.5.0.sha256sum] = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
SRC_URI[dispatch-0.2.0.sha256sum] = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
SRC_URI[displaydoc-0.2.5.sha256sum] = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
SRC_URI[doc-comment-0.3.3.sha256sum] = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
SRC_URI[dotenvy-0.15.7.sha256sum] = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
SRC_URI[dunce-1.0.5.sha256sum] = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
SRC_URI[dyn-clone-1.0.19.sha256sum] = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
SRC_URI[either-1.15.0.sha256sum] = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
SRC_URI[encode_unicode-1.0.0.sha256sum] = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
SRC_URI[encoding_rs-0.8.35.sha256sum] = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
SRC_URI[encoding_rs_io-0.1.7.sha256sum] = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
SRC_URI[endi-1.1.0.sha256sum] = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
SRC_URI[enumflags2-0.7.12.sha256sum] = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
SRC_URI[enumflags2_derive-0.7.12.sha256sum] = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
SRC_URI[env_filter-0.1.3.sha256sum] = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
SRC_URI[env_home-0.1.0.sha256sum] = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
SRC_URI[env_logger-0.11.8.sha256sum] = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
SRC_URI[equivalent-1.0.2.sha256sum] = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
SRC_URI[erased-serde-0.4.6.sha256sum] = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
SRC_URI[errno-0.3.13.sha256sum] = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
SRC_URI[etcetera-0.10.0.sha256sum] = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
SRC_URI[event-listener-5.4.0.sha256sum] = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
SRC_URI[event-listener-strategy-0.5.4.sha256sum] = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
SRC_URI[fastrand-2.3.0.sha256sum] = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
SRC_URI[fdeflate-0.3.7.sha256sum] = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
SRC_URI[filetime-0.2.26.sha256sum] = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
SRC_URI[fixedbitset-0.5.7.sha256sum] = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
SRC_URI[flate2-1.1.2.sha256sum] = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
SRC_URI[float-cmp-0.9.0.sha256sum] = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
SRC_URI[float-cmp-0.10.0.sha256sum] = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
SRC_URI[fnv-1.0.7.sha256sum] = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
SRC_URI[foldhash-0.1.5.sha256sum] = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
SRC_URI[foldhash-0.2.0.sha256sum] = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
SRC_URI[fontconfig-parser-0.5.8.sha256sum] = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
SRC_URI[fontdb-0.12.0.sha256sum] = "ff20bef7942a72af07104346154a70a70b089c572e454b41bef6eb6cb10e9c06"
SRC_URI[form_urlencoded-1.2.2.sha256sum] = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
SRC_URI[fs-err-3.1.1.sha256sum] = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683"
SRC_URI[fs2-0.4.3.sha256sum] = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
SRC_URI[futures-0.3.31.sha256sum] = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
SRC_URI[futures-channel-0.3.31.sha256sum] = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
SRC_URI[futures-core-0.3.31.sha256sum] = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
SRC_URI[futures-executor-0.3.31.sha256sum] = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
SRC_URI[futures-io-0.3.31.sha256sum] = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
SRC_URI[futures-lite-2.6.0.sha256sum] = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
SRC_URI[futures-macro-0.3.31.sha256sum] = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
SRC_URI[futures-sink-0.3.31.sha256sum] = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
SRC_URI[futures-task-0.3.31.sha256sum] = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
SRC_URI[futures-util-0.3.31.sha256sum] = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
SRC_URI[generic-array-0.14.7.sha256sum] = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
SRC_URI[getrandom-0.2.16.sha256sum] = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
SRC_URI[getrandom-0.3.3.sha256sum] = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
SRC_URI[gif-0.12.0.sha256sum] = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
SRC_URI[gimli-0.31.1.sha256sum] = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
SRC_URI[glob-0.3.3.sha256sum] = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
SRC_URI[globset-0.4.16.sha256sum] = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
SRC_URI[globwalk-0.9.1.sha256sum] = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
SRC_URI[gloo-timers-0.3.0.sha256sum] = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
SRC_URI[goblin-0.10.1.sha256sum] = "d6a80adfd63bd7ffd94fefc3d22167880c440a724303080e5aa686fa36abaa96"
SRC_URI[h2-0.4.12.sha256sum] = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
SRC_URI[half-2.6.0.sha256sum] = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
SRC_URI[hashbrown-0.14.5.sha256sum] = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
SRC_URI[hashbrown-0.15.5.sha256sum] = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
SRC_URI[hashbrown-0.16.0.sha256sum] = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
SRC_URI[heck-0.5.0.sha256sum] = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
SRC_URI[hermit-abi-0.5.2.sha256sum] = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
SRC_URI[hex-0.4.3.sha256sum] = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
SRC_URI[hkdf-0.12.4.sha256sum] = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
SRC_URI[hmac-0.12.1.sha256sum] = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
SRC_URI[home-0.5.11.sha256sum] = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
SRC_URI[homedir-0.3.6.sha256sum] = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
SRC_URI[html-escape-0.2.13.sha256sum] = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
SRC_URI[http-1.3.1.sha256sum] = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
SRC_URI[http-body-1.0.1.sha256sum] = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
SRC_URI[http-body-util-0.1.3.sha256sum] = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
SRC_URI[http-content-range-0.2.3.sha256sum] = "63f67baaf67a9ae8fae78ecee69294d552b764dbcd6f8735d0a9c9be20ab0c82"
SRC_URI[httparse-1.10.1.sha256sum] = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
SRC_URI[httpdate-1.0.3.sha256sum] = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
SRC_URI[hyper-1.7.0.sha256sum] = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
SRC_URI[hyper-rustls-0.27.7.sha256sum] = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
SRC_URI[hyper-util-0.1.16.sha256sum] = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
SRC_URI[icu_collections-2.0.0.sha256sum] = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
SRC_URI[icu_locale_core-2.0.0.sha256sum] = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
SRC_URI[icu_normalizer-2.0.0.sha256sum] = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
SRC_URI[icu_normalizer_data-2.0.0.sha256sum] = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
SRC_URI[icu_properties-2.0.1.sha256sum] = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
SRC_URI[icu_properties_data-2.0.1.sha256sum] = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
SRC_URI[icu_provider-2.0.0.sha256sum] = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
SRC_URI[idna-1.1.0.sha256sum] = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
SRC_URI[idna_adapter-1.2.1.sha256sum] = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
SRC_URI[ignore-0.4.23.sha256sum] = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
SRC_URI[image-0.25.6.sha256sum] = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
SRC_URI[imagesize-0.11.0.sha256sum] = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf"
SRC_URI[indexmap-2.10.0.sha256sum] = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
SRC_URI[indicatif-0.18.0.sha256sum] = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
SRC_URI[indoc-2.0.6.sha256sum] = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
SRC_URI[inout-0.1.4.sha256sum] = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
SRC_URI[insta-1.43.2.sha256sum] = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
SRC_URI[io-uring-0.7.9.sha256sum] = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
SRC_URI[ipnet-2.11.0.sha256sum] = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
SRC_URI[iri-string-0.7.8.sha256sum] = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
SRC_URI[is-docker-0.2.0.sha256sum] = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
SRC_URI[is-terminal-0.4.16.sha256sum] = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
SRC_URI[is-wsl-0.4.0.sha256sum] = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
SRC_URI[is_ci-1.2.0.sha256sum] = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
SRC_URI[is_terminal_polyfill-1.70.1.sha256sum] = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
SRC_URI[itertools-0.10.5.sha256sum] = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
SRC_URI[itertools-0.13.0.sha256sum] = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
SRC_URI[itertools-0.14.0.sha256sum] = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
SRC_URI[itoa-1.0.15.sha256sum] = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
SRC_URI[jiff-0.2.15.sha256sum] = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
SRC_URI[jiff-static-0.2.15.sha256sum] = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
SRC_URI[jiff-tzdb-0.1.4.sha256sum] = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
SRC_URI[jiff-tzdb-platform-0.1.3.sha256sum] = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
SRC_URI[jobserver-0.1.33.sha256sum] = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
SRC_URI[jpeg-decoder-0.3.2.sha256sum] = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
SRC_URI[js-sys-0.3.77.sha256sum] = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
SRC_URI[junction-1.2.0.sha256sum] = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16"
SRC_URI[kurbo-0.8.3.sha256sum] = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
SRC_URI[kurbo-0.9.5.sha256sum] = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b"
SRC_URI[lazy_static-1.5.0.sha256sum] = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
SRC_URI[libc-0.2.175.sha256sum] = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
SRC_URI[libmimalloc-sys-0.1.43.sha256sum] = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
SRC_URI[libredox-0.1.6.sha256sum] = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
SRC_URI[libz-rs-sys-0.5.1.sha256sum] = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
SRC_URI[linux-raw-sys-0.4.15.sha256sum] = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
SRC_URI[linux-raw-sys-0.9.4.sha256sum] = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
SRC_URI[litemap-0.8.0.sha256sum] = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
SRC_URI[lock_api-0.4.13.sha256sum] = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
SRC_URI[log-0.4.27.sha256sum] = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
SRC_URI[lru-slab-0.1.2.sha256sum] = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
SRC_URI[lzma-rs-0.3.0.sha256sum] = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
SRC_URI[lzma-sys-0.1.20.sha256sum] = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
SRC_URI[mailparse-0.16.1.sha256sum] = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
SRC_URI[markdown-1.0.0.sha256sum] = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
SRC_URI[matchers-0.2.0.sha256sum] = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
SRC_URI[md-5-0.10.6.sha256sum] = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
SRC_URI[memchr-2.7.5.sha256sum] = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
SRC_URI[memmap2-0.5.10.sha256sum] = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
SRC_URI[memmap2-0.9.7.sha256sum] = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
SRC_URI[memoffset-0.9.1.sha256sum] = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
SRC_URI[miette-7.6.0.sha256sum] = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
SRC_URI[miette-derive-7.6.0.sha256sum] = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
SRC_URI[mimalloc-0.1.47.sha256sum] = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
SRC_URI[mime-0.3.17.sha256sum] = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
SRC_URI[mime_guess-2.0.5.sha256sum] = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
SRC_URI[miniz_oxide-0.8.9.sha256sum] = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
SRC_URI[mio-1.0.4.sha256sum] = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
SRC_URI[miow-0.6.1.sha256sum] = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
SRC_URI[munge-0.4.5.sha256sum] = "9cce144fab80fbb74ec5b89d1ca9d41ddf6b644ab7e986f7d3ed0aab31625cb1"
SRC_URI[munge_macro-0.4.5.sha256sum] = "574af9cd5b9971cbfdf535d6a8d533778481b241c447826d976101e0149392a1"
SRC_URI[nanoid-0.4.0.sha256sum] = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
SRC_URI[nix-0.29.0.sha256sum] = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
SRC_URI[nix-0.30.1.sha256sum] = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
SRC_URI[normalize-line-endings-0.3.0.sha256sum] = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
SRC_URI[nu-ansi-term-0.50.1.sha256sum] = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
SRC_URI[num-0.4.3.sha256sum] = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
SRC_URI[num-bigint-0.4.6.sha256sum] = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
SRC_URI[num-complex-0.4.6.sha256sum] = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
SRC_URI[num-integer-0.1.46.sha256sum] = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
SRC_URI[num-iter-0.1.45.sha256sum] = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
SRC_URI[num-rational-0.4.2.sha256sum] = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
SRC_URI[num-traits-0.2.19.sha256sum] = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
SRC_URI[num_cpus-1.17.0.sha256sum] = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
SRC_URI[object-0.36.7.sha256sum] = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
SRC_URI[once_cell-1.21.3.sha256sum] = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
SRC_URI[once_cell_polyfill-1.70.1.sha256sum] = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
SRC_URI[oorandom-11.1.5.sha256sum] = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
SRC_URI[open-5.3.2.sha256sum] = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
SRC_URI[openssl-probe-0.1.6.sha256sum] = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
SRC_URI[option-ext-0.2.0.sha256sum] = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
SRC_URI[ordered-stream-0.2.0.sha256sum] = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
SRC_URI[os_str_bytes-6.6.1.sha256sum] = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
SRC_URI[owo-colors-4.2.2.sha256sum] = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
SRC_URI[parking-2.2.1.sha256sum] = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
SRC_URI[parking_lot-0.12.4.sha256sum] = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
SRC_URI[parking_lot_core-0.9.11.sha256sum] = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
SRC_URI[paste-1.0.15.sha256sum] = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
SRC_URI[path-slash-0.2.1.sha256sum] = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
SRC_URI[pathdiff-0.2.3.sha256sum] = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
SRC_URI[percent-encoding-2.3.2.sha256sum] = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
SRC_URI[pest-2.8.1.sha256sum] = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
SRC_URI[pest_derive-2.8.1.sha256sum] = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
SRC_URI[pest_generator-2.8.1.sha256sum] = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
SRC_URI[pest_meta-2.8.1.sha256sum] = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
SRC_URI[petgraph-0.8.2.sha256sum] = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca"
SRC_URI[pico-args-0.5.0.sha256sum] = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
SRC_URI[pin-project-1.1.10.sha256sum] = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
SRC_URI[pin-project-internal-1.1.10.sha256sum] = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
SRC_URI[pin-project-lite-0.2.16.sha256sum] = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
SRC_URI[pin-utils-0.1.0.sha256sum] = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
SRC_URI[pkg-config-0.3.32.sha256sum] = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
SRC_URI[plain-0.2.3.sha256sum] = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
SRC_URI[png-0.17.16.sha256sum] = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
SRC_URI[poloto-19.1.2.sha256sum] = "164dbd541c9832e92fa34452e9c2e98b515a548a3f8549fb2402fe1cd5e46b96"
SRC_URI[portable-atomic-1.11.1.sha256sum] = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
SRC_URI[portable-atomic-util-0.2.4.sha256sum] = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
SRC_URI[potential_utf-0.1.2.sha256sum] = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
SRC_URI[ppv-lite86-0.2.21.sha256sum] = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
SRC_URI[predicates-3.1.3.sha256sum] = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
SRC_URI[predicates-core-1.0.9.sha256sum] = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
SRC_URI[predicates-tree-1.0.12.sha256sum] = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
SRC_URI[pretty_assertions-1.4.1.sha256sum] = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
SRC_URI[priority-queue-2.5.0.sha256sum] = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970"
SRC_URI[proc-macro-crate-3.3.0.sha256sum] = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
SRC_URI[proc-macro2-1.0.101.sha256sum] = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
SRC_URI[procfs-0.17.0.sha256sum] = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
SRC_URI[procfs-core-0.17.0.sha256sum] = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
SRC_URI[ptr_meta-0.3.0.sha256sum] = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90"
SRC_URI[ptr_meta_derive-0.3.0.sha256sum] = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1"
SRC_URI[quinn-0.11.8.sha256sum] = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
SRC_URI[quinn-proto-0.11.12.sha256sum] = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
SRC_URI[quinn-udp-0.5.13.sha256sum] = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
SRC_URI[quote-1.0.40.sha256sum] = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
SRC_URI[quoted_printable-0.5.1.sha256sum] = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
SRC_URI[r-efi-5.3.0.sha256sum] = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
SRC_URI[rancor-0.1.0.sha256sum] = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947"
SRC_URI[rand-0.8.5.sha256sum] = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
SRC_URI[rand-0.9.2.sha256sum] = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
SRC_URI[rand_chacha-0.3.1.sha256sum] = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
SRC_URI[rand_chacha-0.9.0.sha256sum] = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
SRC_URI[rand_core-0.6.4.sha256sum] = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
SRC_URI[rand_core-0.9.3.sha256sum] = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
SRC_URI[rayon-1.10.0.sha256sum] = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
SRC_URI[rayon-core-1.12.1.sha256sum] = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
SRC_URI[rctree-0.5.0.sha256sum] = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
SRC_URI[redox_syscall-0.5.15.sha256sum] = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
SRC_URI[redox_users-0.5.0.sha256sum] = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
SRC_URI[ref-cast-1.0.24.sha256sum] = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
SRC_URI[ref-cast-impl-1.0.24.sha256sum] = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
SRC_URI[reflink-copy-0.1.28.sha256sum] = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92"
SRC_URI[regex-1.11.2.sha256sum] = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
SRC_URI[regex-automata-0.4.10.sha256sum] = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
SRC_URI[regex-syntax-0.8.5.sha256sum] = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
SRC_URI[rend-0.5.2.sha256sum] = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215"
SRC_URI[reqwest-0.12.22.sha256sum] = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
SRC_URI[resvg-0.29.0.sha256sum] = "76888219c0881e22b0ceab06fddcfe83163cd81642bd60c7842387f9c968a72e"
SRC_URI[retry-policies-0.4.0.sha256sum] = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c"
SRC_URI[rgb-0.8.52.sha256sum] = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
SRC_URI[ring-0.17.14.sha256sum] = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
SRC_URI[rkyv-0.8.11.sha256sum] = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac"
SRC_URI[rkyv_derive-0.8.11.sha256sum] = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22"
SRC_URI[rmp-0.8.14.sha256sum] = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
SRC_URI[rmp-serde-1.3.0.sha256sum] = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
SRC_URI[rosvgtree-0.1.0.sha256sum] = "bdc23d1ace03d6b8153c7d16f0708cd80b61ee8e80304954803354e67e40d150"
SRC_URI[roxmltree-0.18.1.sha256sum] = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302"
SRC_URI[roxmltree-0.20.0.sha256sum] = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
SRC_URI[rust-netrc-0.1.2.sha256sum] = "7e98097f62769f92dbf95fb51f71c0a68ec18a4ee2e70e0d3e4f47ac005d63e9"
SRC_URI[rustc-demangle-0.1.25.sha256sum] = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
SRC_URI[rustc-hash-2.1.1.sha256sum] = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
SRC_URI[rustix-0.38.44.sha256sum] = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
SRC_URI[rustix-1.0.8.sha256sum] = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
SRC_URI[rustls-0.23.29.sha256sum] = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
SRC_URI[rustls-native-certs-0.8.1.sha256sum] = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
SRC_URI[rustls-pki-types-1.12.0.sha256sum] = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
SRC_URI[rustls-webpki-0.103.4.sha256sum] = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
SRC_URI[rustversion-1.0.21.sha256sum] = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
SRC_URI[rustybuzz-0.7.0.sha256sum] = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a"
SRC_URI[ryu-1.0.20.sha256sum] = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
SRC_URI[same-file-1.0.6.sha256sum] = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
SRC_URI[schannel-0.1.27.sha256sum] = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
SRC_URI[schemars-1.0.4.sha256sum] = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
SRC_URI[schemars_derive-1.0.4.sha256sum] = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
SRC_URI[scopeguard-1.2.0.sha256sum] = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
SRC_URI[scroll-0.13.0.sha256sum] = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
SRC_URI[scroll_derive-0.13.0.sha256sum] = "22fc4f90c27b57691bbaf11d8ecc7cfbfe98a4da6dbe60226115d322aa80c06e"
SRC_URI[seahash-4.1.0.sha256sum] = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
SRC_URI[secrecy-0.10.3.sha256sum] = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
SRC_URI[secret-service-5.0.0.sha256sum] = "dccff79e916a339eec808de579764e3459658c903960d5aa4f7959ee9f6d5f2b"
SRC_URI[security-framework-3.2.0.sha256sum] = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
SRC_URI[security-framework-sys-2.14.0.sha256sum] = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
SRC_URI[self-replace-1.5.0.sha256sum] = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
SRC_URI[semver-1.0.26.sha256sum] = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
SRC_URI[serde-1.0.223.sha256sum] = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
SRC_URI[serde-untagged-0.1.9.sha256sum] = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
SRC_URI[serde_core-1.0.223.sha256sum] = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
SRC_URI[serde_derive-1.0.223.sha256sum] = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
SRC_URI[serde_derive_internals-0.29.1.sha256sum] = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
SRC_URI[serde_json-1.0.145.sha256sum] = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
SRC_URI[serde_repr-0.1.20.sha256sum] = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
SRC_URI[serde_spanned-1.0.0.sha256sum] = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
SRC_URI[serde_urlencoded-0.7.1.sha256sum] = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
SRC_URI[serde_yaml-0.9.34+deprecated.sha256sum] = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
SRC_URI[sha2-0.10.9.sha256sum] = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
SRC_URI[sharded-slab-0.1.7.sha256sum] = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
SRC_URI[shell-escape-0.1.5.sha256sum] = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
SRC_URI[shellexpand-3.1.1.sha256sum] = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
SRC_URI[shlex-1.3.0.sha256sum] = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
SRC_URI[signal-hook-registry-1.4.5.sha256sum] = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
SRC_URI[simd-adler32-0.3.7.sha256sum] = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
SRC_URI[simdutf8-0.1.5.sha256sum] = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
SRC_URI[similar-2.7.0.sha256sum] = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
SRC_URI[simplecss-0.2.2.sha256sum] = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
SRC_URI[siphasher-0.3.11.sha256sum] = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
SRC_URI[slab-0.4.10.sha256sum] = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
SRC_URI[smallvec-1.15.1.sha256sum] = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
SRC_URI[smawk-0.3.2.sha256sum] = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
SRC_URI[socket2-0.5.10.sha256sum] = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
SRC_URI[socket2-0.6.0.sha256sum] = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
SRC_URI[spdx-0.10.9.sha256sum] = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
SRC_URI[stable_deref_trait-1.2.0.sha256sum] = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
SRC_URI[static_assertions-1.1.0.sha256sum] = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
SRC_URI[statrs-0.18.0.sha256sum] = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
SRC_URI[strict-num-0.1.1.sha256sum] = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
SRC_URI[strsim-0.11.1.sha256sum] = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
SRC_URI[subtle-2.6.1.sha256sum] = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
SRC_URI[supports-color-3.0.2.sha256sum] = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
SRC_URI[supports-hyperlinks-3.1.0.sha256sum] = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
SRC_URI[supports-unicode-3.0.0.sha256sum] = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
SRC_URI[svg-0.18.0.sha256sum] = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3"
SRC_URI[svgfilters-0.4.0.sha256sum] = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce"
SRC_URI[svgtypes-0.9.0.sha256sum] = "c9ee29c1407a5b18ccfe5f6ac82ac11bab3b14407e09c209a6c1a32098b19734"
SRC_URI[svgtypes-0.10.0.sha256sum] = "98ffacedcdcf1da6579c907279b4f3c5492fbce99fbbf227f5ed270a589c2765"
SRC_URI[syn-2.0.106.sha256sum] = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
SRC_URI[sync_wrapper-1.0.2.sha256sum] = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
SRC_URI[synstructure-0.13.2.sha256sum] = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
SRC_URI[sys-info-0.9.1.sha256sum] = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c"
SRC_URI[system-configuration-0.6.1.sha256sum] = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
SRC_URI[system-configuration-sys-0.6.0.sha256sum] = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
SRC_URI[tagu-0.1.6.sha256sum] = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a"
SRC_URI[tar-0.4.44.sha256sum] = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
SRC_URI[target-lexicon-0.13.3.sha256sum] = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
SRC_URI[temp-env-0.3.6.sha256sum] = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
SRC_URI[tempfile-3.20.0.sha256sum] = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
SRC_URI[terminal_size-0.4.2.sha256sum] = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
SRC_URI[termtree-0.5.1.sha256sum] = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
SRC_URI[test-case-3.3.1.sha256sum] = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
SRC_URI[test-case-core-3.3.1.sha256sum] = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
SRC_URI[test-case-macros-3.3.1.sha256sum] = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
SRC_URI[test-log-0.2.18.sha256sum] = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b"
SRC_URI[test-log-macros-0.2.18.sha256sum] = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36"
SRC_URI[textwrap-0.16.2.sha256sum] = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
SRC_URI[thiserror-1.0.69.sha256sum] = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
SRC_URI[thiserror-2.0.16.sha256sum] = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
SRC_URI[thiserror-impl-1.0.69.sha256sum] = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
SRC_URI[thiserror-impl-2.0.16.sha256sum] = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
SRC_URI[thread_local-1.1.9.sha256sum] = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
SRC_URI[tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7.sha256sum] = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
SRC_URI[tikv-jemallocator-0.6.0.sha256sum] = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
SRC_URI[tiny-skia-0.8.4.sha256sum] = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67"
SRC_URI[tiny-skia-path-0.8.4.sha256sum] = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c"
SRC_URI[tinystr-0.8.1.sha256sum] = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
SRC_URI[tinytemplate-1.2.1.sha256sum] = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
SRC_URI[tinyvec-1.9.0.sha256sum] = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
SRC_URI[tinyvec_macros-0.1.1.sha256sum] = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
SRC_URI[tokio-1.47.1.sha256sum] = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
SRC_URI[tokio-macros-2.5.0.sha256sum] = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
SRC_URI[tokio-rustls-0.26.2.sha256sum] = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
SRC_URI[tokio-stream-0.1.17.sha256sum] = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
SRC_URI[tokio-util-0.7.15.sha256sum] = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
SRC_URI[toml-0.9.5.sha256sum] = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
SRC_URI[toml_datetime-0.6.11.sha256sum] = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
SRC_URI[toml_datetime-0.7.0.sha256sum] = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
SRC_URI[toml_edit-0.22.27.sha256sum] = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
SRC_URI[toml_edit-0.23.4.sha256sum] = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93"
SRC_URI[toml_parser-1.0.2.sha256sum] = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
SRC_URI[toml_writer-1.0.2.sha256sum] = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
SRC_URI[tower-0.5.2.sha256sum] = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
SRC_URI[tower-http-0.6.6.sha256sum] = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
SRC_URI[tower-layer-0.3.3.sha256sum] = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
SRC_URI[tower-service-0.3.3.sha256sum] = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
SRC_URI[tracing-0.1.41.sha256sum] = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
SRC_URI[tracing-attributes-0.1.30.sha256sum] = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
SRC_URI[tracing-core-0.1.34.sha256sum] = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
SRC_URI[tracing-durations-export-0.3.1.sha256sum] = "32e0c2cfee378f62291f2703bbb949b99213306c2729fe977799653c3c3404b5"
SRC_URI[tracing-log-0.2.0.sha256sum] = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
SRC_URI[tracing-serde-0.2.0.sha256sum] = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
SRC_URI[tracing-subscriber-0.3.20.sha256sum] = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
SRC_URI[tracing-test-0.2.5.sha256sum] = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68"
SRC_URI[tracing-test-macro-0.2.5.sha256sum] = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
SRC_URI[tracing-tree-0.4.0.sha256sum] = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c"
SRC_URI[try-lock-0.2.5.sha256sum] = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
SRC_URI[ttf-parser-0.18.1.sha256sum] = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633"
SRC_URI[typeid-1.0.3.sha256sum] = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
SRC_URI[typenum-1.18.0.sha256sum] = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
SRC_URI[ucd-trie-0.1.7.sha256sum] = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
SRC_URI[uds_windows-1.1.0.sha256sum] = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
SRC_URI[unicase-2.8.1.sha256sum] = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
SRC_URI[unicode-bidi-0.3.18.sha256sum] = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
SRC_URI[unicode-bidi-mirroring-0.1.0.sha256sum] = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
SRC_URI[unicode-ccc-0.1.2.sha256sum] = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
SRC_URI[unicode-general-category-0.6.0.sha256sum] = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7"
SRC_URI[unicode-id-0.3.5.sha256sum] = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
SRC_URI[unicode-ident-1.0.18.sha256sum] = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
SRC_URI[unicode-linebreak-0.1.5.sha256sum] = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
SRC_URI[unicode-script-0.5.7.sha256sum] = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
SRC_URI[unicode-vo-0.1.0.sha256sum] = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
SRC_URI[unicode-width-0.1.14.sha256sum] = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
SRC_URI[unicode-width-0.2.1.sha256sum] = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
SRC_URI[unit-prefix-0.5.1.sha256sum] = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
SRC_URI[unsafe-libyaml-0.2.11.sha256sum] = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
SRC_URI[unscanny-0.1.0.sha256sum] = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"
SRC_URI[untrusted-0.9.0.sha256sum] = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
SRC_URI[url-2.5.7.sha256sum] = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
SRC_URI[usvg-0.29.0.sha256sum] = "63b6bb4e62619d9f68aa2d8a823fea2bff302340a1f2d45c264d5b0be170832e"
SRC_URI[usvg-text-layout-0.29.0.sha256sum] = "195386e01bc35f860db024de275a76e7a31afdf975d18beb6d0e44764118b4db"
SRC_URI[utf8-width-0.1.7.sha256sum] = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
SRC_URI[utf8_iter-1.0.4.sha256sum] = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
SRC_URI[utf8parse-0.2.2.sha256sum] = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
SRC_URI[uuid-1.17.0.sha256sum] = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
SRC_URI[valuable-0.1.1.sha256sum] = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
SRC_URI[version_check-0.9.5.sha256sum] = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
SRC_URI[wait-timeout-0.2.1.sha256sum] = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
SRC_URI[walkdir-2.5.0.sha256sum] = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
SRC_URI[want-0.3.1.sha256sum] = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
SRC_URI[wasi-0.11.1+wasi-snapshot-preview1.sha256sum] = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
SRC_URI[wasi-0.14.2+wasi-0.2.4.sha256sum] = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
SRC_URI[wasite-0.1.0.sha256sum] = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
SRC_URI[wasm-bindgen-0.2.100.sha256sum] = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
SRC_URI[wasm-bindgen-backend-0.2.100.sha256sum] = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
SRC_URI[wasm-bindgen-futures-0.4.50.sha256sum] = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
SRC_URI[wasm-bindgen-macro-0.2.100.sha256sum] = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
SRC_URI[wasm-bindgen-macro-support-0.2.100.sha256sum] = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
SRC_URI[wasm-bindgen-shared-0.2.100.sha256sum] = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
SRC_URI[wasm-streams-0.4.2.sha256sum] = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
SRC_URI[wasmtimer-0.4.2.sha256sum] = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446"
SRC_URI[web-sys-0.3.77.sha256sum] = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
SRC_URI[web-time-1.1.0.sha256sum] = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
SRC_URI[webpki-roots-1.0.2.sha256sum] = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
SRC_URI[weezl-0.1.10.sha256sum] = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
SRC_URI[which-8.0.0.sha256sum] = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
SRC_URI[whoami-1.6.1.sha256sum] = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
SRC_URI[widestring-1.2.0.sha256sum] = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
SRC_URI[winapi-0.3.9.sha256sum] = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
SRC_URI[winapi-i686-pc-windows-gnu-0.4.0.sha256sum] = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
SRC_URI[winapi-util-0.1.9.sha256sum] = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
SRC_URI[winapi-x86_64-pc-windows-gnu-0.4.0.sha256sum] = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
SRC_URI[windows-0.59.0.sha256sum] = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
SRC_URI[windows-0.61.3.sha256sum] = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
SRC_URI[windows-collections-0.2.0.sha256sum] = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
SRC_URI[windows-core-0.59.0.sha256sum] = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
SRC_URI[windows-core-0.61.2.sha256sum] = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
SRC_URI[windows-future-0.2.1.sha256sum] = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
SRC_URI[windows-implement-0.59.0.sha256sum] = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
SRC_URI[windows-implement-0.60.0.sha256sum] = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
SRC_URI[windows-interface-0.59.1.sha256sum] = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
SRC_URI[windows-link-0.1.3.sha256sum] = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
SRC_URI[windows-link-0.2.0.sha256sum] = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
SRC_URI[windows-numerics-0.2.0.sha256sum] = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
SRC_URI[windows-registry-0.5.3.sha256sum] = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
SRC_URI[windows-result-0.3.4.sha256sum] = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
SRC_URI[windows-strings-0.3.1.sha256sum] = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
SRC_URI[windows-strings-0.4.2.sha256sum] = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
SRC_URI[windows-sys-0.52.0.sha256sum] = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
SRC_URI[windows-sys-0.59.0.sha256sum] = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
SRC_URI[windows-sys-0.60.2.sha256sum] = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
SRC_URI[windows-sys-0.61.0.sha256sum] = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
SRC_URI[windows-targets-0.52.6.sha256sum] = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
SRC_URI[windows-targets-0.53.2.sha256sum] = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
SRC_URI[windows-threading-0.1.0.sha256sum] = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
SRC_URI[windows_aarch64_gnullvm-0.52.6.sha256sum] = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
SRC_URI[windows_aarch64_gnullvm-0.53.0.sha256sum] = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
SRC_URI[windows_aarch64_msvc-0.52.6.sha256sum] = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
SRC_URI[windows_aarch64_msvc-0.53.0.sha256sum] = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
SRC_URI[windows_i686_gnu-0.52.6.sha256sum] = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
SRC_URI[windows_i686_gnu-0.53.0.sha256sum] = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
SRC_URI[windows_i686_gnullvm-0.52.6.sha256sum] = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
SRC_URI[windows_i686_gnullvm-0.53.0.sha256sum] = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
SRC_URI[windows_i686_msvc-0.52.6.sha256sum] = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
SRC_URI[windows_i686_msvc-0.53.0.sha256sum] = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
SRC_URI[windows_x86_64_gnu-0.52.6.sha256sum] = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
SRC_URI[windows_x86_64_gnu-0.53.0.sha256sum] = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
SRC_URI[windows_x86_64_gnullvm-0.52.6.sha256sum] = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
SRC_URI[windows_x86_64_gnullvm-0.53.0.sha256sum] = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
SRC_URI[windows_x86_64_msvc-0.52.6.sha256sum] = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
SRC_URI[windows_x86_64_msvc-0.53.0.sha256sum] = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
SRC_URI[winnow-0.7.12.sha256sum] = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
SRC_URI[winsafe-0.0.19.sha256sum] = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
SRC_URI[wiremock-0.6.5.sha256sum] = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
SRC_URI[wit-bindgen-rt-0.39.0.sha256sum] = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
SRC_URI[writeable-0.6.1.sha256sum] = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
SRC_URI[xattr-1.5.1.sha256sum] = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
SRC_URI[xmlparser-0.13.6.sha256sum] = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
SRC_URI[xz2-0.1.7.sha256sum] = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
SRC_URI[yansi-1.0.1.sha256sum] = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
SRC_URI[yoke-0.8.0.sha256sum] = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
SRC_URI[yoke-derive-0.8.0.sha256sum] = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
SRC_URI[zbus-5.8.0.sha256sum] = "597f45e98bc7e6f0988276012797855613cd8269e23b5be62cc4e5d28b7e515d"
SRC_URI[zbus_macros-5.8.0.sha256sum] = "e5c8e4e14dcdd9d97a98b189cd1220f30e8394ad271e8c987da84f73693862c2"
SRC_URI[zbus_names-4.2.0.sha256sum] = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
SRC_URI[zerocopy-0.8.26.sha256sum] = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
SRC_URI[zerocopy-derive-0.8.26.sha256sum] = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
SRC_URI[zerofrom-0.1.6.sha256sum] = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
SRC_URI[zerofrom-derive-0.1.6.sha256sum] = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
SRC_URI[zeroize-1.8.1.sha256sum] = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
SRC_URI[zerotrie-0.2.2.sha256sum] = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
SRC_URI[zerovec-0.11.2.sha256sum] = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
SRC_URI[zerovec-derive-0.11.1.sha256sum] = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
SRC_URI[zip-2.4.2.sha256sum] = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
SRC_URI[zlib-rs-0.5.1.sha256sum] = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
SRC_URI[zopfli-0.8.2.sha256sum] = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
SRC_URI[zstd-0.13.3.sha256sum] = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
SRC_URI[zstd-safe-7.2.4.sha256sum] = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
SRC_URI[zstd-sys-2.0.15+zstd.1.5.7.sha256sum] = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
SRC_URI[zvariant-5.6.0.sha256sum] = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f"
SRC_URI[zvariant_derive-5.6.0.sha256sum] = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208"
SRC_URI[zvariant_utils-3.2.0.sha256sum] = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
# from crates/uv-performance-memory-allocator/Cargo.lock
SRC_URI += " \
    crate://crates.io/cc/1.2.5 \
    crate://crates.io/libc/0.2.169 \
    crate://crates.io/libmimalloc-sys/0.1.44 \
    crate://crates.io/mimalloc/0.1.48 \
    crate://crates.io/shlex/1.3.0 \
    crate://crates.io/tikv-jemalloc-sys/0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7 \
    crate://crates.io/tikv-jemallocator/0.6.0 \
"

SRC_URI[cc-1.2.5.sha256sum] = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
SRC_URI[libc-0.2.169.sha256sum] = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
SRC_URI[libmimalloc-sys-0.1.44.sha256sum] = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
SRC_URI[mimalloc-0.1.48.sha256sum] = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
SRC_URI[shlex-1.3.0.sha256sum] = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
SRC_URI[tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7.sha256sum] = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
SRC_URI[tikv-jemallocator-0.6.0.sha256sum] = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
Stefan Herbrechtsmeier Oct. 10, 2025, 6:27 a.m. UTC | #10
Am 09.10.2025 um 16:30 schrieb Gyorgy Sarvari:
> On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
>> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>>> as a source crate, which is most often fetched from a git repository.
>>>>>
>>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>>> however with Yocto this task is taken over by Bitbake.
>>>>>
>>>>> After fetching these crates, they are made available to cargo by adding the location
>>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>>> that can be found in the SRC_URI is added as one source crate.
>>>>>
>>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>>
>>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>>
>>>>> This change introduces the following:
>>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>>     this was that maturin seems to ignore source crate patches from config.toml)
>>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>>     deleted git repository lines from it)
>>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>>     the separate crate folders are copied into this folder, and it is used as the central
>>>>>     vendoring folder. This is needed for source replacements: the folder that is used for
>>>>>     vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>>     has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>>     given manifest is a workspace AND a package at once)
>>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>>     to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>>     Having destsuffix is still mandatory though.
>>>>>
>>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>>> I use a similar approach for my Cargo.lock fetcher. In my case the code
>>>> finds the crate on the fly inside the a git repository because the
>>>> Cargo.lock doesn't contain the subpath.
>>> By any chance, did you manage to solve the workspace problem? If you
>>> have a working solution, feel free to submit it, I wouldn't mind if I
>>> wouldn't have to debug mine :D
>> I haven't test a workspace project. Do you have an example project?
>>
> I have attached a sample recipe (that is very much based on Tom Geelen's
> initial work). It depends on at least 2 workspaces.

Thanks for the sample. After switching to my cargolock fecher and 
cargo_vendor class the project build without problems. Your git URLs 
need a parameter to inform the config generate that the source contains 
a rev query parameter. Additionally you need to add the revision to the 
name and destsuffix/subdir because it is possible to use crates with 
different revisions from the same repository.

>>>>> Signed-off-by: Gyorgy Sarvari<skandigraun@gmail.com>
>>>>> Cc: Tom Geelen<t.f.g.geelen@gmail.com>
>>>>>
>>>>> ---
>>>>>    meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>>    1 file changed, 108 insertions(+), 50 deletions(-)
>>>>>
>>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>>> index c9eb2d09a5..79c1351298 100644
>>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>>    python cargo_common_do_patch_paths() {
>>>>>        import shutil
>>>>>    
>>>>> +    def is_rust_crate_folder(path):
>>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>>> +        return os.path.exists(cargo_toml_path)
>>>>> +
>>>>> +    def load_toml_file(toml_path):
>>>>> +        import tomllib
>>>>> +        with open(toml_path, 'rb') as f:
>>>>> +            toml = tomllib.load(f)
>>>>> +        return toml
>>>>> +
>>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>>> +        for lf_repo in lockfile_repos.keys():
>>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>>> Does this works if the URL contains a "rev" query parameter? This
>>>> happens if the same git repository is used with different revisions.
>>> I *think* yes, since I query the revision from the fetcher, instead of
>>> parsing it myself (and I use both the repo and revision for matching the
>>> cargo.lock repos). But will test it specifically, and make it work if it
>>> wouldn't work out of the box. Thanks for calling my attention on this.
>> The problem is that the source replacement key contains a query
>> parameter. The query isn't supported by the git fetcher. That means
>> you have to remove the query from the SRC_URI but add it back in the
>> source entry in the config.toml.
> You mean for dynamic fetching, from Cargo.lock? This patch still relies
> on the user adding these dependencies to the SRC_URI.
> Otherwise I might be misunderstanding your question...

Please check the source inside the Cargo.lock:
https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L302

It contains a rev query parameter. This query parameter must be part of 
the source key inside the config.toml:

[source."git+https://github.com/astral-sh/rs-async-zip?rev=285e48742b74ab109887d62e1ae79e7c15fd4878"]


>>>>> +                lockfile_repos[lf_repo] = True
>>>>> +                return lf_repo.split("#")[0]
>>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>>> +
>>>>> +    def create_cargo_checksum(folder_path):
>>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>>> +        if os.path.exists(checksum_path):
>>>>> +            return
>>>>> +
>>>>> +        import hashlib, json
>>>>> +
>>>>> +        checksum = {'files': {}}
>>>>> +        for root, _, files in os.walk(folder_path):
>>>>> +            for f in files:
>>>>> +                full_path = os.path.join(root, f)
>>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>>> +                if relative_path.startswith(".git/"):
>>>>> +                    continue
>>>>> +                with open(full_path, 'rb') as f2:
>>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>>> +                checksum["files"][relative_path] = file_sha
>>>> Do we really need the calculation of the checksum?
>>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>>> (But I'd be happy to stand corrected)
>> Have you test an empty dictionary for "files" and NULL for "package"?
>>
> Are these valid states? Currently the checksum calculation happens for
> crate folders that have been actually copied to the vendor folder. And
> that happens only, in case there is at least a Cargo.toml manifest in
> that folder, so the files dict shouldn't be empty. Otherwise the
> checksum sub iterates through all the files it can find, it doesn't try
> to validate it against any manifests.

Do we need the validation by cargo? The crate fetcher skip the 
validation with an empty dict and the same works for git sources.

>>>>> +
>>>>> +        with open(checksum_path, 'w') as f:
>>>>> +            json.dump(checksum, f)
>>>>> +
>>>>>        cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>>        if not os.path.exists(cargo_config):
>>>>>            return
>>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>>        if len(src_uri) == 0:
>>>>>            return
>>>>>    
>>>>> -    patches = dict()
>>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>> +    if not os.path.exists(lockfile):
>>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>>> +
>>>>> +    lockfile = load_toml_file(lockfile)
>>>>> +
>>>>> +    # key is the repo url, value is a boolean, which is used later
>>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>>> +    lockfile_git_repos = {}
>>>>> +    for p in lockfile['package']:
>>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>>> +            lockfile_git_repos[p['source']] = False
>>>>> +
>>>>> +    sources = dict()
>>>>>        workdir = d.getVar('UNPACKDIR')
>>>>>        fetcher = bb.fetch2.Fetch(src_uri, d)
>>>>> +
>>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>>> +
>>>>> +    os.makedirs(vendor_folder)
>>>>> +
>>>>>        for url in fetcher.urls:
>>>>>            ud = fetcher.ud[url]
>>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>>> -            name = ud.parm.get('name')
>>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>>> -            if name is not None and destsuffix is not None:
>>>>> -                if ud.user:
>>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>> -                else:
>>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>>> -                patches.setdefault(repo, []).append(path)
>>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>>> +            continue
>>>>>    
>>>>> -    with open(cargo_config, "a+") as config:
>>>>> -        for k, v in patches.items():
>>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>>> -            for name in v:
>>>>> -                print(name, file=config)
>>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>>    
>>>>> -    if not patches:
>>>>> -        return
>>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>>> +            continue
>>>>>    
>>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>>> -    # downloaded by the fetch steps are those expected by the
>>>>> -    # project and that the possible patches are correctly applied.
>>>>> -    # Moreover since we do not want any modification
>>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>>> +        if ud.user:
>>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>> +        else:
>>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>    
>>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>> -    if not os.path.exists(lockfile):
>>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>>> +
>>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>>> +
>>>>> +        if 'workspace' in cargo_toml:
>>>>> +            members = cargo_toml['workspace']['members']
>>>>> +            for member in members:
>>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>>> +
>>>>> +        if 'package' in cargo_toml:
>>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>>> +            crate_name = cargo_toml['package']['name']
>>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>>> +
>>>>> +    for d in os.scandir(vendor_folder):
>>>>> +        if d.is_dir():
>>>>> +            create_cargo_checksum(d.path)
>>>>> +
>>>>> +
>>>>> +    with open(cargo_config, "a+") as config:
>>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>>> +
>>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>>> +            print('git = "%s"' % repo, file=config)
>>>>> +            print('rev = "%s"' % revision, file=config)
>>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>>> +
>>>>> +    # check if there are any git repos in the lock file that were not visited
>>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>>> +        if not found_in_src_uri:
>>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>>    
>>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>>> -    # --frozen so let's handle that modifications here.
>>>>> -    #
>>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>>> -    # patched packages:
>>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>>> -    # But this is not possible since it requires that cargo local git db
>>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>>> -
>>>>> -    lockfile_orig = lockfile + ".orig"
>>>>> -    if not os.path.exists(lockfile_orig):
>>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>>> -
>>>>> -    newlines = []
>>>>> -    with open(lockfile_orig, "r") as f:
>>>>> -        for line in f.readlines():
>>>>> -            if not line.startswith("source = \"git"):
>>>>> -                newlines.append(line)
>>>>> -
>>>>> -    with open(lockfile, "w") as f:
>>>>> -        f.writelines(newlines)
>>>>>    }
>>>>> +
>>>>>    do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>>    
>>>>>    do_compile:prepend () {
>>>>>
>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>> Links: You receive all messages sent to this group.
>>>>> View/Reply Online (#224426):https://lists.openembedded.org/g/openembedded-core/message/224426
>>>>> Mute This Topic:https://lists.openembedded.org/mt/115578466/6374899
>>>>> Group Owner:openembedded-core+owner@lists.openembedded.org
>>>>> Unsubscribe:https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>
Gyorgy Sarvari Oct. 10, 2025, 8:04 a.m. UTC | #11
On 10/10/25 08:27, Stefan Herbrechtsmeier wrote:
> Am 09.10.2025 um 16:30 schrieb Gyorgy Sarvari:
>> On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
>>> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>>>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>>>> as a source crate, which is most often fetched from a git repository.
>>>>>>
>>>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>>>> however with Yocto this task is taken over by Bitbake.
>>>>>>
>>>>>> After fetching these crates, they are made available to cargo by adding the location
>>>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>>>> that can be found in the SRC_URI is added as one source crate.
>>>>>>
>>>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>>>
>>>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>>>
>>>>>> This change introduces the following:
>>>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>>>    this was that maturin seems to ignore source crate patches from config.toml)
>>>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>>>    deleted git repository lines from it)
>>>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>>>    the separate crate folders are copied into this folder, and it is used as the central
>>>>>>    vendoring folder. This is needed for source replacements: the folder that is used for
>>>>>>    vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>>>    has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>>>    given manifest is a workspace AND a package at once)
>>>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>>>    to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>>>    Having destsuffix is still mandatory though.
>>>>>>
>>>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>>>> I use a similar approach for my Cargo.lock fetcher. In my case the code 
>>>>> finds the crate on the fly inside the a git repository because the 
>>>>> Cargo.lock doesn't contain the subpath.
>>>> By any chance, did you manage to solve the workspace problem? If you
>>>> have a working solution, feel free to submit it, I wouldn't mind if I
>>>> wouldn't have to debug mine :D
>>> I haven't test a workspace project. Do you have an example project?
>>>
>> I have attached a sample recipe (that is very much based on Tom Geelen's
>> initial work). It depends on at least 2 workspaces.
>
> Thanks for the sample. After switching to my cargolock fecher and
> cargo_vendor class the project build without problems. Your git URLs
> need a parameter to inform the config generate that the source
> contains a rev query parameter. Additionally you need to add the
> revision to the name and destsuffix/subdir because it is possible to
> use crates with different revisions from the same repository.
>

Yes, but the name and destsuffix already need to be always unique by
definition for each SRC_URI component. If name isn't unique, then you
can't specify different revisions. And git fetcher (and I suspect others
too) starts fetching with deleting the target folder, so if destsuffix
isn't unique, then only the last code prevails. If this should be
enforced, I would rather put that logic into a new QA check, or maybe
into the fetcher directly.

(A bit more touched on your note below)

>>>>>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>>>>>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>>>>>
>>>>>> ---
>>>>>>   meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>>>   1 file changed, 108 insertions(+), 50 deletions(-)
>>>>>>
>>>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>>>> index c9eb2d09a5..79c1351298 100644
>>>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>>>   python cargo_common_do_patch_paths() {
>>>>>>       import shutil
>>>>>>   
>>>>>> +    def is_rust_crate_folder(path):
>>>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>>>> +        return os.path.exists(cargo_toml_path)
>>>>>> +
>>>>>> +    def load_toml_file(toml_path):
>>>>>> +        import tomllib
>>>>>> +        with open(toml_path, 'rb') as f:
>>>>>> +            toml = tomllib.load(f)
>>>>>> +        return toml
>>>>>> +
>>>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>>>> +        for lf_repo in lockfile_repos.keys():
>>>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>>>> Does this works if the URL contains a "rev" query parameter? This 
>>>>> happens if the same git repository is used with different revisions.
>>>> I *think* yes, since I query the revision from the fetcher, instead of
>>>> parsing it myself (and I use both the repo and revision for matching the
>>>> cargo.lock repos). But will test it specifically, and make it work if it
>>>> wouldn't work out of the box. Thanks for calling my attention on this.
>>> The problem is that the source replacement key contains a query
>>> parameter. The query isn't supported by the git fetcher. That means
>>> you have to remove the query from the SRC_URI but add it back in the
>>> source entry in the config.toml.
>> You mean for dynamic fetching, from Cargo.lock? This patch still relies
>> on the user adding these dependencies to the SRC_URI.
>> Otherwise I might be misunderstanding your question...
>
> Please check the source inside the Cargo.lock:
> https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L302
>
> It contains a rev query parameter. This query parameter must be part
> of the source key inside the config.toml:
>
> [source."git+https://github.com/astral-sh/rs-async-zip?rev=285e48742b74ab109887d62e1ae79e7c15fd4878"]
>
>

Yes, it supposed to work like that already. The
extract_git_repos_from_lockfile() collects the repos starting with
"git+" from Cargo.lock - this value contains the rev parameter already.
These values are only used as keys in the config.toml file, just like
your example. Before adding it to config.toml, only the last optional
part, the revision after the "#" is cut off. (E.g.
"git+https://github.com/foo?rev=123#123" becomes only
"git+https://github.com/foo?rev=123")

I do not use SRC_URI components in this file - currently there is some
loose connection between SRC_URI and Cargo.lock repos: at the end I try
to match each SRC_URI to a Cargo.lock repo, and see if the user has
fetched every required repo (by trying to match the repo URL and
revision), but in general the values are used differently.

>>>>>> +                lockfile_repos[lf_repo] = True
>>>>>> +                return lf_repo.split("#")[0]
>>>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>>>> +
>>>>>> +    def create_cargo_checksum(folder_path):
>>>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>>>> +        if os.path.exists(checksum_path):
>>>>>> +            return
>>>>>> +
>>>>>> +        import hashlib, json
>>>>>> +
>>>>>> +        checksum = {'files': {}}
>>>>>> +        for root, _, files in os.walk(folder_path):
>>>>>> +            for f in files:
>>>>>> +                full_path = os.path.join(root, f)
>>>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>>>> +                if relative_path.startswith(".git/"):
>>>>>> +                    continue
>>>>>> +                with open(full_path, 'rb') as f2:
>>>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>>>> +                checksum["files"][relative_path] = file_sha
>>>>> Do we really need the calculation of the checksum?
>>>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>>>> (But I'd be happy to stand corrected)
>>> Have you test an empty dictionary for "files" and NULL for "package"?
>>>
>> Are these valid states? Currently the checksum calculation happens for
>> crate folders that have been actually copied to the vendor folder. And
>> that happens only, in case there is at least a Cargo.toml manifest in
>> that folder, so the files dict shouldn't be empty. Otherwise the
>> checksum sub iterates through all the files it can find, it doesn't try
>> to validate it against any manifests.
>
> Do we need the validation by cargo? The crate fetcher skip the
> validation with an empty dict and the same works for git sources.
>

Oh, you mean if there are no sources found that should be vendored? That
was definitely a bug in this version, and v2 should have a check for
that - if there is nothing to vendor than it stops and returns silently.

>>>>>> +
>>>>>> +        with open(checksum_path, 'w') as f:
>>>>>> +            json.dump(checksum, f)
>>>>>> +
>>>>>>       cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>>>       if not os.path.exists(cargo_config):
>>>>>>           return
>>>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>>>       if len(src_uri) == 0:
>>>>>>           return
>>>>>>   
>>>>>> -    patches = dict()
>>>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>> +    if not os.path.exists(lockfile):
>>>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>> +
>>>>>> +    lockfile = load_toml_file(lockfile)
>>>>>> +
>>>>>> +    # key is the repo url, value is a boolean, which is used later
>>>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>>>> +    lockfile_git_repos = {}
>>>>>> +    for p in lockfile['package']:
>>>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>>>> +            lockfile_git_repos[p['source']] = False
>>>>>> +
>>>>>> +    sources = dict()
>>>>>>       workdir = d.getVar('UNPACKDIR')
>>>>>>       fetcher = bb.fetch2.Fetch(src_uri, d)
>>>>>> +
>>>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>>>> +
>>>>>> +    os.makedirs(vendor_folder)
>>>>>> +
>>>>>>       for url in fetcher.urls:
>>>>>>           ud = fetcher.ud[url]
>>>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>>>> -            name = ud.parm.get('name')
>>>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>>>> -            if name is not None and destsuffix is not None:
>>>>>> -                if ud.user:
>>>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>> -                else:
>>>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>>>> -                patches.setdefault(repo, []).append(path)
>>>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>>>> +            continue
>>>>>>   
>>>>>> -    with open(cargo_config, "a+") as config:
>>>>>> -        for k, v in patches.items():
>>>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>>>> -            for name in v:
>>>>>> -                print(name, file=config)
>>>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>>>   
>>>>>> -    if not patches:
>>>>>> -        return
>>>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>>>> +            continue
>>>>>>   
>>>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>>>> -    # downloaded by the fetch steps are those expected by the
>>>>>> -    # project and that the possible patches are correctly applied.
>>>>>> -    # Moreover since we do not want any modification
>>>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>>>> +        if ud.user:
>>>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>> +        else:
>>>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>   
>>>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>> -    if not os.path.exists(lockfile):
>>>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>>>> +
>>>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>>>> +
>>>>>> +        if 'workspace' in cargo_toml:
>>>>>> +            members = cargo_toml['workspace']['members']
>>>>>> +            for member in members:
>>>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>>>> +
>>>>>> +        if 'package' in cargo_toml:
>>>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>>>> +            crate_name = cargo_toml['package']['name']
>>>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>>>> +
>>>>>> +    for d in os.scandir(vendor_folder):
>>>>>> +        if d.is_dir():
>>>>>> +            create_cargo_checksum(d.path)
>>>>>> +
>>>>>> +
>>>>>> +    with open(cargo_config, "a+") as config:
>>>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>>>> +
>>>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>>>> +            print('git = "%s"' % repo, file=config)
>>>>>> +            print('rev = "%s"' % revision, file=config)
>>>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>>>> +
>>>>>> +    # check if there are any git repos in the lock file that were not visited
>>>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>>>> +        if not found_in_src_uri:
>>>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>>>   
>>>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>>>> -    # --frozen so let's handle that modifications here.
>>>>>> -    #
>>>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>>>> -    # patched packages:
>>>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>>>> -    # But this is not possible since it requires that cargo local git db
>>>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>>>> -
>>>>>> -    lockfile_orig = lockfile + ".orig"
>>>>>> -    if not os.path.exists(lockfile_orig):
>>>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>>>> -
>>>>>> -    newlines = []
>>>>>> -    with open(lockfile_orig, "r") as f:
>>>>>> -        for line in f.readlines():
>>>>>> -            if not line.startswith("source = \"git"):
>>>>>> -                newlines.append(line)
>>>>>> -
>>>>>> -    with open(lockfile, "w") as f:
>>>>>> -        f.writelines(newlines)
>>>>>>   }
>>>>>> +
>>>>>>   do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>>>   
>>>>>>   do_compile:prepend () {
>>>>>>
>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>> Links: You receive all messages sent to this group.
>>>>>> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
>>>>>> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
>>>>>> Group Owner: openembedded-core+owner@lists.openembedded.org
>>>>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>
Stefan Herbrechtsmeier Oct. 10, 2025, 10:38 a.m. UTC | #12
Am 10.10.2025 um 10:04 schrieb Gyorgy Sarvari:
> On 10/10/25 08:27, Stefan Herbrechtsmeier wrote:
>> Am 09.10.2025 um 16:30 schrieb Gyorgy Sarvari:
>>> On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
>>>> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>>>>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>>>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>>>>> as a source crate, which is most often fetched from a git repository.
>>>>>>>
>>>>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>>>>> however with Yocto this task is taken over by Bitbake.
>>>>>>>
>>>>>>> After fetching these crates, they are made available to cargo by adding the location
>>>>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>>>>> that can be found in the SRC_URI is added as one source crate.
>>>>>>>
>>>>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>>>>
>>>>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>>>>
>>>>>>> This change introduces the following:
>>>>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>>>>     this was that maturin seems to ignore source crate patches from config.toml)
>>>>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>>>>     deleted git repository lines from it)
>>>>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>>>>     the separate crate folders are copied into this folder, and it is used as the central
>>>>>>>     vendoring folder. This is needed for source replacements: the folder that is used for
>>>>>>>     vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>>>>     has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>>>>     given manifest is a workspace AND a package at once)
>>>>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>>>>     to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>>>>     Having destsuffix is still mandatory though.
>>>>>>>
>>>>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>>>>> I use a similar approach for my Cargo.lock fetcher. In my case the code
>>>>>> finds the crate on the fly inside the a git repository because the
>>>>>> Cargo.lock doesn't contain the subpath.
>>>>> By any chance, did you manage to solve the workspace problem? If you
>>>>> have a working solution, feel free to submit it, I wouldn't mind if I
>>>>> wouldn't have to debug mine :D
>>>> I haven't test a workspace project. Do you have an example project?
>>>>
>>> I have attached a sample recipe (that is very much based on Tom Geelen's
>>> initial work). It depends on at least 2 workspaces.
>> Thanks for the sample. After switching to my cargolock fecher and
>> cargo_vendor class the project build without problems. Your git URLs
>> need a parameter to inform the config generate that the source
>> contains a rev query parameter. Additionally you need to add the
>> revision to the name and destsuffix/subdir because it is possible to
>> use crates with different revisions from the same repository.
>>
> Yes, but the name and destsuffix already need to be always unique by
> definition for each SRC_URI component. If name isn't unique, then you
> can't specify different revisions. And git fetcher (and I suspect others
> too) starts fetching with deleting the target folder, so if destsuffix
> isn't unique, then only the last code prevails.
It was only a comment, that the solution inside your example doesn't 
work in any case.

In your example the Cargo.lock reference two crates inside the same 
repository:

[[package]]
name = "reqwest-middleware"
version = "0.4.2"
source = 
"git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"

[[package]]
name = "reqwest-retry"
version = "0.7.0"
source = 
"git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"

(https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L3431)

Luckily both use the same revision of the repository and you can use a 
single SRC_URI:

SRC_URI += 
"git://github.com/astral-sh/reqwest-middleware;protocol=https;name=reqwest-middleware;destsuffix=reqwest-middleware;branch=main"

If they use different revisions you have to add two SRC_URIs with 
different name and destsuffix. Furthermore you need to add the crates 
from the correct destsuffix to the vendor folder because both checkouts 
can contain the same crates.

I follow the cargo vendor approach and use a part of the revision inside 
the subdir (destsuffix).

SRC_URI += 
"git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#7650ed7;destsuffix=cargo/git/checkouts/reqwest-middleware/7650ed7"
SRC_URI += 
"git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#591ab44;destsuffix=cargo/git/checkouts/reqwest-middleware/591ab44"

> If this should be
> enforced, I would rather put that logic into a new QA check, or maybe
> into the fetcher directly.
>
> (A bit more touched on your note below)
>
>>>>>>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>>>>>>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>>>>>>
>>>>>>> ---
>>>>>>>    meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>>>>    1 file changed, 108 insertions(+), 50 deletions(-)
>>>>>>>
>>>>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>>>>> index c9eb2d09a5..79c1351298 100644
>>>>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>>>>    python cargo_common_do_patch_paths() {
>>>>>>>        import shutil
>>>>>>>    
>>>>>>> +    def is_rust_crate_folder(path):
>>>>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>>>>> +        return os.path.exists(cargo_toml_path)
>>>>>>> +
>>>>>>> +    def load_toml_file(toml_path):
>>>>>>> +        import tomllib
>>>>>>> +        with open(toml_path, 'rb') as f:
>>>>>>> +            toml = tomllib.load(f)
>>>>>>> +        return toml
>>>>>>> +
>>>>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>>>>> +        for lf_repo in lockfile_repos.keys():
>>>>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>>>>> Does this works if the URL contains a "rev" query parameter? This
>>>>>> happens if the same git repository is used with different revisions.
>>>>> I *think* yes, since I query the revision from the fetcher, instead of
>>>>> parsing it myself (and I use both the repo and revision for matching the
>>>>> cargo.lock repos). But will test it specifically, and make it work if it
>>>>> wouldn't work out of the box. Thanks for calling my attention on this.
>>>> The problem is that the source replacement key contains a query
>>>> parameter. The query isn't supported by the git fetcher. That means
>>>> you have to remove the query from the SRC_URI but add it back in the
>>>> source entry in the config.toml.
>>> You mean for dynamic fetching, from Cargo.lock? This patch still relies
>>> on the user adding these dependencies to the SRC_URI.
>>> Otherwise I might be misunderstanding your question...
>> Please check the source inside the Cargo.lock:
>> https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L302
>>
>> It contains a rev query parameter. This query parameter must be part
>> of the source key inside the config.toml:
>>
>> [source."git+https://github.com/astral-sh/rs-async-zip?rev=285e48742b74ab109887d62e1ae79e7c15fd4878"]
>>
>>
> Yes, it supposed to work like that already. The
> extract_git_repos_from_lockfile() collects the repos starting with
> "git+" from Cargo.lock - this value contains the rev parameter already.
> These values are only used as keys in the config.toml file, just like
> your example. Before adding it to config.toml, only the last optional
> part, the revision after the "#" is cut off. (E.g.
> "git+https://github.com/foo?rev=123#123" becomes only
> "git+https://github.com/foo?rev=123")
Okay, I use the SRC_URIs as source and therefore have the problem to 
reconstruct the source key. The reason therefore where that I populate 
the vendor folder during do_unpack. But the config.toml could be created 
after do_patch and the Cargo.lock should be the better source.

> I do not use SRC_URI components in this file - currently there is some
> loose connection between SRC_URI and Cargo.lock repos: at the end I try
> to match each SRC_URI to a Cargo.lock repo, and see if the user has
> fetched every required repo (by trying to match the repo URL and
> revision), but in general the values are used differently.
>
>>>>>>> +                lockfile_repos[lf_repo] = True
>>>>>>> +                return lf_repo.split("#")[0]
>>>>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>>>>> +
>>>>>>> +    def create_cargo_checksum(folder_path):
>>>>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>>>>> +        if os.path.exists(checksum_path):
>>>>>>> +            return
>>>>>>> +
>>>>>>> +        import hashlib, json
>>>>>>> +
>>>>>>> +        checksum = {'files': {}}
>>>>>>> +        for root, _, files in os.walk(folder_path):
>>>>>>> +            for f in files:
>>>>>>> +                full_path = os.path.join(root, f)
>>>>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>>>>> +                if relative_path.startswith(".git/"):
>>>>>>> +                    continue
>>>>>>> +                with open(full_path, 'rb') as f2:
>>>>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>>>>> +                checksum["files"][relative_path] = file_sha
>>>>>> Do we really need the calculation of the checksum?
>>>>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>>>>> (But I'd be happy to stand corrected)
>>>> Have you test an empty dictionary for "files" and NULL for "package"?
>>>>
>>> Are these valid states? Currently the checksum calculation happens for
>>> crate folders that have been actually copied to the vendor folder. And
>>> that happens only, in case there is at least a Cargo.toml manifest in
>>> that folder, so the files dict shouldn't be empty. Otherwise the
>>> checksum sub iterates through all the files it can find, it doesn't try
>>> to validate it against any manifests.
>> Do we need the validation by cargo? The crate fetcher skip the
>> validation with an empty dict and the same works for git sources.
>>
> Oh, you mean if there are no sources found that should be vendored? That
> was definitely a bug in this version, and v2 should have a check for
> that - if there is nothing to vendor than it stops and returns silently.

I mean that we already ensure the integrity of our sources. Why should 
we invest time to populate the files checksum if we could simple skip 
the check inside cargo by an empty files dictionary.

metadata['files'] = {}
metadata['package'] = tarhash

(https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L120)

Maybe we should even remove the .cargo-checksum.json creation from the 
fetcher and move it into your function. We can speed up the process if 
we use the checksum from the Cargo.lock instead of the on-the-fly 
calculation inside the fetcher:

tarhash = hashlib.sha256(f.read()).hexdigest()

(https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L118)

>>>>>>> +
>>>>>>> +        with open(checksum_path, 'w') as f:
>>>>>>> +            json.dump(checksum, f)
>>>>>>> +
>>>>>>>        cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>>>>        if not os.path.exists(cargo_config):
>>>>>>>            return
>>>>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>>>>        if len(src_uri) == 0:
>>>>>>>            return
>>>>>>>    
>>>>>>> -    patches = dict()
>>>>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>> +    if not os.path.exists(lockfile):
>>>>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>> +
>>>>>>> +    lockfile = load_toml_file(lockfile)
>>>>>>> +
>>>>>>> +    # key is the repo url, value is a boolean, which is used later
>>>>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>>>>> +    lockfile_git_repos = {}
>>>>>>> +    for p in lockfile['package']:
>>>>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>>>>> +            lockfile_git_repos[p['source']] = False
>>>>>>> +
>>>>>>> +    sources = dict()
>>>>>>>        workdir = d.getVar('UNPACKDIR')
>>>>>>>        fetcher = bb.fetch2.Fetch(src_uri, d)
>>>>>>> +
>>>>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>>>>> +
>>>>>>> +    os.makedirs(vendor_folder)
>>>>>>> +
>>>>>>>        for url in fetcher.urls:
>>>>>>>            ud = fetcher.ud[url]
>>>>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>>>>> -            name = ud.parm.get('name')
>>>>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>>>>> -            if name is not None and destsuffix is not None:
>>>>>>> -                if ud.user:
>>>>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>> -                else:
>>>>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>>>>> -                patches.setdefault(repo, []).append(path)
>>>>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>>>>> +            continue
>>>>>>>    
>>>>>>> -    with open(cargo_config, "a+") as config:
>>>>>>> -        for k, v in patches.items():
>>>>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>>>>> -            for name in v:
>>>>>>> -                print(name, file=config)
>>>>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>>>>    
>>>>>>> -    if not patches:
>>>>>>> -        return
>>>>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>>>>> +            continue
>>>>>>>    
>>>>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>>>>> -    # downloaded by the fetch steps are those expected by the
>>>>>>> -    # project and that the possible patches are correctly applied.
>>>>>>> -    # Moreover since we do not want any modification
>>>>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>>>>> +        if ud.user:
>>>>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>> +        else:
>>>>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>>    
>>>>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>> -    if not os.path.exists(lockfile):
>>>>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>>>>> +
>>>>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>>>>> +
>>>>>>> +        if 'workspace' in cargo_toml:
>>>>>>> +            members = cargo_toml['workspace']['members']
>>>>>>> +            for member in members:
>>>>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>>>>> +
>>>>>>> +        if 'package' in cargo_toml:
>>>>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>>>>> +            crate_name = cargo_toml['package']['name']
>>>>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>>>>> +
>>>>>>> +    for d in os.scandir(vendor_folder):
>>>>>>> +        if d.is_dir():
>>>>>>> +            create_cargo_checksum(d.path)
>>>>>>> +
>>>>>>> +
>>>>>>> +    with open(cargo_config, "a+") as config:
>>>>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>>>>> +
>>>>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>>>>> +            print('git = "%s"' % repo, file=config)
>>>>>>> +            print('rev = "%s"' % revision, file=config)
>>>>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>>>>> +
>>>>>>> +    # check if there are any git repos in the lock file that were not visited
>>>>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>>>>> +        if not found_in_src_uri:
>>>>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>>>>    
>>>>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>>>>> -    # --frozen so let's handle that modifications here.
>>>>>>> -    #
>>>>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>>>>> -    # patched packages:
>>>>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>>>>> -    # But this is not possible since it requires that cargo local git db
>>>>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>>>>> -
>>>>>>> -    lockfile_orig = lockfile + ".orig"
>>>>>>> -    if not os.path.exists(lockfile_orig):
>>>>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>>>>> -
>>>>>>> -    newlines = []
>>>>>>> -    with open(lockfile_orig, "r") as f:
>>>>>>> -        for line in f.readlines():
>>>>>>> -            if not line.startswith("source = \"git"):
>>>>>>> -                newlines.append(line)
>>>>>>> -
>>>>>>> -    with open(lockfile, "w") as f:
>>>>>>> -        f.writelines(newlines)
>>>>>>>    }
>>>>>>> +
>>>>>>>    do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>>>>    
>>>>>>>    do_compile:prepend () {
>>>>>>>
>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>> Links: You receive all messages sent to this group.
>>>>>>> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
>>>>>>> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
>>>>>>> Group Owner: openembedded-core+owner@lists.openembedded.org
>>>>>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>>
Gyorgy Sarvari Oct. 10, 2025, 11:35 a.m. UTC | #13
On 10/10/25 12:38, Stefan Herbrechtsmeier wrote:
> Am 10.10.2025 um 10:04 schrieb Gyorgy Sarvari:
>> On 10/10/25 08:27, Stefan Herbrechtsmeier wrote:
>>> Am 09.10.2025 um 16:30 schrieb Gyorgy Sarvari:
>>>> On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
>>>>> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>>>>>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>>>>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>>>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>>>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>>>>>> as a source crate, which is most often fetched from a git repository.
>>>>>>>>
>>>>>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>>>>>> however with Yocto this task is taken over by Bitbake.
>>>>>>>>
>>>>>>>> After fetching these crates, they are made available to cargo by adding the location
>>>>>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>>>>>> that can be found in the SRC_URI is added as one source crate.
>>>>>>>>
>>>>>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>>>>>
>>>>>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>>>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>>>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>>>>>
>>>>>>>> This change introduces the following:
>>>>>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>>>>>     this was that maturin seems to ignore source crate patches from config.toml)
>>>>>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>>>>>     deleted git repository lines from it)
>>>>>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>>>>>     the separate crate folders are copied into this folder, and it is used as the central
>>>>>>>>     vendoring folder. This is needed for source replacements: the folder that is used for
>>>>>>>>     vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>>>>>     has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>>>>>     given manifest is a workspace AND a package at once)
>>>>>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>>>>>     to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>>>>>     Having destsuffix is still mandatory though.
>>>>>>>>
>>>>>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>>>>>> I use a similar approach for my Cargo.lock fetcher. In my case the code
>>>>>>> finds the crate on the fly inside the a git repository because the
>>>>>>> Cargo.lock doesn't contain the subpath.
>>>>>> By any chance, did you manage to solve the workspace problem? If you
>>>>>> have a working solution, feel free to submit it, I wouldn't mind if I
>>>>>> wouldn't have to debug mine :D
>>>>> I haven't test a workspace project. Do you have an example project?
>>>>>
>>>> I have attached a sample recipe (that is very much based on Tom Geelen's
>>>> initial work). It depends on at least 2 workspaces.
>>> Thanks for the sample. After switching to my cargolock fecher and
>>> cargo_vendor class the project build without problems. Your git URLs
>>> need a parameter to inform the config generate that the source
>>> contains a rev query parameter. Additionally you need to add the
>>> revision to the name and destsuffix/subdir because it is possible to
>>> use crates with different revisions from the same repository.
>>>
>> Yes, but the name and destsuffix already need to be always unique by
>> definition for each SRC_URI component. If name isn't unique, then you
>> can't specify different revisions. And git fetcher (and I suspect others
>> too) starts fetching with deleting the target folder, so if destsuffix
>> isn't unique, then only the last code prevails.
> It was only a comment, that the solution inside your example doesn't 
> work in any case.
>
> In your example the Cargo.lock reference two crates inside the same 
> repository:
>
> [[package]]
> name = "reqwest-middleware"
> version = "0.4.2"
> source = 
> "git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"
>
> [[package]]
> name = "reqwest-retry"
> version = "0.7.0"
> source = 
> "git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"
>
> (https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L3431)
>
> Luckily both use the same revision of the repository and you can use a 
> single SRC_URI:
>
> SRC_URI += 
> "git://github.com/astral-sh/reqwest-middleware;protocol=https;name=reqwest-middleware;destsuffix=reqwest-middleware;branch=main"
>
> If they use different revisions you have to add two SRC_URIs with 
> different name and destsuffix. Furthermore you need to add the crates 
> from the correct destsuffix to the vendor folder because both checkouts 
> can contain the same crates.
>
> I follow the cargo vendor approach and use a part of the revision inside 
> the subdir (destsuffix).
>
> SRC_URI += 
> "git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#7650ed7;destsuffix=cargo/git/checkouts/reqwest-middleware/7650ed7"
> SRC_URI += 
> "git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#591ab44;destsuffix=cargo/git/checkouts/reqwest-middleware/591ab44"

Hmmm... ooops. You are definitely right - I do break crates if they have
the same name, they are not differentiated and copied over each other.
Thanks a lot - will take care of it in next version.

Regarding the duplicated SRC_URI: yes, but that's on the user.

>> If this should be
>> enforced, I would rather put that logic into a new QA check, or maybe
>> into the fetcher directly.
>>
>> (A bit more touched on your note below)
>>
>>>>>>>> Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
>>>>>>>> Cc: Tom Geelen <t.f.g.geelen@gmail.com>
>>>>>>>>
>>>>>>>> ---
>>>>>>>>    meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>>>>>    1 file changed, 108 insertions(+), 50 deletions(-)
>>>>>>>>
>>>>>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>>>>>> index c9eb2d09a5..79c1351298 100644
>>>>>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>>>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>>>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>>>>>    python cargo_common_do_patch_paths() {
>>>>>>>>        import shutil
>>>>>>>>    
>>>>>>>> +    def is_rust_crate_folder(path):
>>>>>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>>>>>> +        return os.path.exists(cargo_toml_path)
>>>>>>>> +
>>>>>>>> +    def load_toml_file(toml_path):
>>>>>>>> +        import tomllib
>>>>>>>> +        with open(toml_path, 'rb') as f:
>>>>>>>> +            toml = tomllib.load(f)
>>>>>>>> +        return toml
>>>>>>>> +
>>>>>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>>>>>> +        for lf_repo in lockfile_repos.keys():
>>>>>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>>>>>> Does this works if the URL contains a "rev" query parameter? This
>>>>>>> happens if the same git repository is used with different revisions.
>>>>>> I *think* yes, since I query the revision from the fetcher, instead of
>>>>>> parsing it myself (and I use both the repo and revision for matching the
>>>>>> cargo.lock repos). But will test it specifically, and make it work if it
>>>>>> wouldn't work out of the box. Thanks for calling my attention on this.
>>>>> The problem is that the source replacement key contains a query
>>>>> parameter. The query isn't supported by the git fetcher. That means
>>>>> you have to remove the query from the SRC_URI but add it back in the
>>>>> source entry in the config.toml.
>>>> You mean for dynamic fetching, from Cargo.lock? This patch still relies
>>>> on the user adding these dependencies to the SRC_URI.
>>>> Otherwise I might be misunderstanding your question...
>>> Please check the source inside the Cargo.lock:
>>> https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L302
>>>
>>> It contains a rev query parameter. This query parameter must be part
>>> of the source key inside the config.toml:
>>>
>>> [source."git+https://github.com/astral-sh/rs-async-zip?rev=285e48742b74ab109887d62e1ae79e7c15fd4878"]
>>>
>>>
>> Yes, it supposed to work like that already. The
>> extract_git_repos_from_lockfile() collects the repos starting with
>> "git+" from Cargo.lock - this value contains the rev parameter already.
>> These values are only used as keys in the config.toml file, just like
>> your example. Before adding it to config.toml, only the last optional
>> part, the revision after the "#" is cut off. (E.g.
>> "git+https://github.com/foo?rev=123#123" becomes only
>> "git+https://github.com/foo?rev=123")
> Okay, I use the SRC_URIs as source and therefore have the problem to 
> reconstruct the source key. The reason therefore where that I populate 
> the vendor folder during do_unpack. But the config.toml could be created 
> after do_patch and the Cargo.lock should be the better source.

Personally my main goal would be only to add workspace support to the
existing class without moving other pieces, but I think config.toml
creation could be moved after do_patch without big issues, I don't see
any obvious dependency problems.
I would still encourage you to submit your solution (if it is possible
to open-source it) - it sounds it has been used in real world builds.

>> I do not use SRC_URI components in this file - currently there is some
>> loose connection between SRC_URI and Cargo.lock repos: at the end I try
>> to match each SRC_URI to a Cargo.lock repo, and see if the user has
>> fetched every required repo (by trying to match the repo URL and
>> revision), but in general the values are used differently.
>>
>>>>>>>> +                lockfile_repos[lf_repo] = True
>>>>>>>> +                return lf_repo.split("#")[0]
>>>>>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>>>>>> +
>>>>>>>> +    def create_cargo_checksum(folder_path):
>>>>>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>>>>>> +        if os.path.exists(checksum_path):
>>>>>>>> +            return
>>>>>>>> +
>>>>>>>> +        import hashlib, json
>>>>>>>> +
>>>>>>>> +        checksum = {'files': {}}
>>>>>>>> +        for root, _, files in os.walk(folder_path):
>>>>>>>> +            for f in files:
>>>>>>>> +                full_path = os.path.join(root, f)
>>>>>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>>>>>> +                if relative_path.startswith(".git/"):
>>>>>>>> +                    continue
>>>>>>>> +                with open(full_path, 'rb') as f2:
>>>>>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>>>>>> +                checksum["files"][relative_path] = file_sha
>>>>>>> Do we really need the calculation of the checksum?
>>>>>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>>>>>> (But I'd be happy to stand corrected)
>>>>> Have you test an empty dictionary for "files" and NULL for "package"?
>>>>>
>>>> Are these valid states? Currently the checksum calculation happens for
>>>> crate folders that have been actually copied to the vendor folder. And
>>>> that happens only, in case there is at least a Cargo.toml manifest in
>>>> that folder, so the files dict shouldn't be empty. Otherwise the
>>>> checksum sub iterates through all the files it can find, it doesn't try
>>>> to validate it against any manifests.
>>> Do we need the validation by cargo? The crate fetcher skip the
>>> validation with an empty dict and the same works for git sources.
>>>
>> Oh, you mean if there are no sources found that should be vendored? That
>> was definitely a bug in this version, and v2 should have a check for
>> that - if there is nothing to vendor than it stops and returns silently.
> I mean that we already ensure the integrity of our sources. Why should 
> we invest time to populate the files checksum if we could simple skip 
> the check inside cargo by an empty files dictionary.
>
> metadata['files'] = {}
> metadata['package'] = tarhash
>
> (https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L120)
>
> Maybe we should even remove the .cargo-checksum.json creation from the 
> fetcher and move it into your function. We can speed up the process if 
> we use the checksum from the Cargo.lock instead of the on-the-fly 
> calculation inside the fetcher:
>
> tarhash = hashlib.sha256(f.read()).hexdigest()
>
> (https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L118)

I haven't seen this part yet in the fetcher, but I can definitely look
into it for simplification. Thanks - will try to add it to the next version.

>>>>>>>> +
>>>>>>>> +        with open(checksum_path, 'w') as f:
>>>>>>>> +            json.dump(checksum, f)
>>>>>>>> +
>>>>>>>>        cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>>>>>        if not os.path.exists(cargo_config):
>>>>>>>>            return
>>>>>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>>>>>        if len(src_uri) == 0:
>>>>>>>>            return
>>>>>>>>    
>>>>>>>> -    patches = dict()
>>>>>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>>> +    if not os.path.exists(lockfile):
>>>>>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>>> +
>>>>>>>> +    lockfile = load_toml_file(lockfile)
>>>>>>>> +
>>>>>>>> +    # key is the repo url, value is a boolean, which is used later
>>>>>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>>>>>> +    lockfile_git_repos = {}
>>>>>>>> +    for p in lockfile['package']:
>>>>>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>>>>>> +            lockfile_git_repos[p['source']] = False
>>>>>>>> +
>>>>>>>> +    sources = dict()
>>>>>>>>        workdir = d.getVar('UNPACKDIR')
>>>>>>>>        fetcher = bb.fetch2.Fetch(src_uri, d)
>>>>>>>> +
>>>>>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>>>>>> +
>>>>>>>> +    os.makedirs(vendor_folder)
>>>>>>>> +
>>>>>>>>        for url in fetcher.urls:
>>>>>>>>            ud = fetcher.ud[url]
>>>>>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>>>>>> -            name = ud.parm.get('name')
>>>>>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>>>>>> -            if name is not None and destsuffix is not None:
>>>>>>>> -                if ud.user:
>>>>>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>>> -                else:
>>>>>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>>>>>> -                patches.setdefault(repo, []).append(path)
>>>>>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>>>>>> +            continue
>>>>>>>>    
>>>>>>>> -    with open(cargo_config, "a+") as config:
>>>>>>>> -        for k, v in patches.items():
>>>>>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>>>>>> -            for name in v:
>>>>>>>> -                print(name, file=config)
>>>>>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>>>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>>>>>    
>>>>>>>> -    if not patches:
>>>>>>>> -        return
>>>>>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>>>>>> +            continue
>>>>>>>>    
>>>>>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>>>>>> -    # downloaded by the fetch steps are those expected by the
>>>>>>>> -    # project and that the possible patches are correctly applied.
>>>>>>>> -    # Moreover since we do not want any modification
>>>>>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>>>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>>>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>>>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>>>>>> +        if ud.user:
>>>>>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>>> +        else:
>>>>>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>>>    
>>>>>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>>> -    if not os.path.exists(lockfile):
>>>>>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>>>>>> +
>>>>>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>>>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>>>>>> +
>>>>>>>> +        if 'workspace' in cargo_toml:
>>>>>>>> +            members = cargo_toml['workspace']['members']
>>>>>>>> +            for member in members:
>>>>>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>>>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>>>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>>>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>>>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>>>>>> +
>>>>>>>> +        if 'package' in cargo_toml:
>>>>>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>>>>>> +            crate_name = cargo_toml['package']['name']
>>>>>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>>>>>> +
>>>>>>>> +    for d in os.scandir(vendor_folder):
>>>>>>>> +        if d.is_dir():
>>>>>>>> +            create_cargo_checksum(d.path)
>>>>>>>> +
>>>>>>>> +
>>>>>>>> +    with open(cargo_config, "a+") as config:
>>>>>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>>>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>>>>>> +
>>>>>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>>>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>>>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>>>>>> +            print('git = "%s"' % repo, file=config)
>>>>>>>> +            print('rev = "%s"' % revision, file=config)
>>>>>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>>>>>> +
>>>>>>>> +    # check if there are any git repos in the lock file that were not visited
>>>>>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>>>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>>>>>> +        if not found_in_src_uri:
>>>>>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>>>>>    
>>>>>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>>>>>> -    # --frozen so let's handle that modifications here.
>>>>>>>> -    #
>>>>>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>>>>>> -    # patched packages:
>>>>>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>>>>>> -    # But this is not possible since it requires that cargo local git db
>>>>>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>>>>>> -
>>>>>>>> -    lockfile_orig = lockfile + ".orig"
>>>>>>>> -    if not os.path.exists(lockfile_orig):
>>>>>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>>>>>> -
>>>>>>>> -    newlines = []
>>>>>>>> -    with open(lockfile_orig, "r") as f:
>>>>>>>> -        for line in f.readlines():
>>>>>>>> -            if not line.startswith("source = \"git"):
>>>>>>>> -                newlines.append(line)
>>>>>>>> -
>>>>>>>> -    with open(lockfile, "w") as f:
>>>>>>>> -        f.writelines(newlines)
>>>>>>>>    }
>>>>>>>> +
>>>>>>>>    do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>>>>>    
>>>>>>>>    do_compile:prepend () {
>>>>>>>>
>>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>>> Links: You receive all messages sent to this group.
>>>>>>>> View/Reply Online (#224426): https://lists.openembedded.org/g/openembedded-core/message/224426
>>>>>>>> Mute This Topic: https://lists.openembedded.org/mt/115578466/6374899
>>>>>>>> Group Owner: openembedded-core+owner@lists.openembedded.org
>>>>>>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>>>
Stefan Herbrechtsmeier Oct. 10, 2025, 5:04 p.m. UTC | #14
Am 10.10.2025 um 13:35 schrieb Gyorgy Sarvari:
> On 10/10/25 12:38, Stefan Herbrechtsmeier wrote:
>> Am 10.10.2025 um 10:04 schrieb Gyorgy Sarvari:
>>> On 10/10/25 08:27, Stefan Herbrechtsmeier wrote:
>>>> Am 09.10.2025 um 16:30 schrieb Gyorgy Sarvari:
>>>>> On 10/9/25 11:31, Stefan Herbrechtsmeier wrote:
>>>>>> Am 08.10.2025 um 13:01 schrieb Gyorgy Sarvari:
>>>>>>> On 10/7/25 16:59, Stefan Herbrechtsmeier wrote:
>>>>>>>> Am 03.10.2025 um 23:30 schrieb Gyorgy Sarvari via lists.openembedded.org:
>>>>>>>>> Cargo.toml files usually contain a list of dependencies in one of two forms:
>>>>>>>>> either a crate name that can be fetched from some registry (like crates.io), or
>>>>>>>>> as a source crate, which is most often fetched from a git repository.
>>>>>>>>>
>>>>>>>>> Normally cargo handles fetching the crates from both the registry and from git,
>>>>>>>>> however with Yocto this task is taken over by Bitbake.
>>>>>>>>>
>>>>>>>>> After fetching these crates, they are made available to cargo by adding the location
>>>>>>>>> to $CARGO_HOME/config.toml. The source crates are of interest here: each git repository
>>>>>>>>> that can be found in the SRC_URI is added as one source crate.
>>>>>>>>>
>>>>>>>>> This works most of the time, as long as the repository really contains one crate only.
>>>>>>>>>
>>>>>>>>> However in case the repository is a cargo workspace, it contains multiple crates in
>>>>>>>>> different subfolders, and in order to allow cargo to process them, they need to be
>>>>>>>>> listed separately. This is not happening with the current implementation of cargo_common.
>>>>>>>>>
>>>>>>>>> This change introduces the following:
>>>>>>>>> - instead of patching the dependencies, use source replacement (the primary motivation for
>>>>>>>>>      this was that maturin seems to ignore source crate patches from config.toml)
>>>>>>>>> - the above also allows to keep the original Cargo.lock untouched (the original implementation
>>>>>>>>>      deleted git repository lines from it)
>>>>>>>>> - it adds a new folder, currently ${UNPACKDIR}/yocto-vendored-source-crates. During processing
>>>>>>>>>      the separate crate folders are copied into this folder, and it is used as the central
>>>>>>>>>      vendoring folder. This is needed for source replacements: the folder that is used for
>>>>>>>>>      vendoring needs to contain the crates separately, one crate in one folder. Each folder
>>>>>>>>>      has the name of the crate that it contains. Workspaces are not included here (unless the
>>>>>>>>>      given manifest is a workspace AND a package at once)
>>>>>>>>> - previuosly the SRC_URI had to contain a "name" and a "destsuffix" parameter to be considered
>>>>>>>>>      to be a rust crate. The name is not derived from the Cargo.toml file, not from the SRC_URI.
>>>>>>>>>      Having destsuffix is still mandatory though.
>>>>>>>>>
>>>>>>>>> The change does not handle nested workspaces, only the top level Cargo.toml is processed.
>>>>>>>> I use a similar approach for my Cargo.lock fetcher. In my case the code
>>>>>>>> finds the crate on the fly inside the a git repository because the
>>>>>>>> Cargo.lock doesn't contain the subpath.
>>>>>>> By any chance, did you manage to solve the workspace problem? If you
>>>>>>> have a working solution, feel free to submit it, I wouldn't mind if I
>>>>>>> wouldn't have to debug mine :D
>>>>>> I haven't test a workspace project. Do you have an example project?
>>>>>>
>>>>> I have attached a sample recipe (that is very much based on Tom Geelen's
>>>>> initial work). It depends on at least 2 workspaces.
>>>> Thanks for the sample. After switching to my cargolock fecher and
>>>> cargo_vendor class the project build without problems. Your git URLs
>>>> need a parameter to inform the config generate that the source
>>>> contains a rev query parameter. Additionally you need to add the
>>>> revision to the name and destsuffix/subdir because it is possible to
>>>> use crates with different revisions from the same repository.
>>>>
>>> Yes, but the name and destsuffix already need to be always unique by
>>> definition for each SRC_URI component. If name isn't unique, then you
>>> can't specify different revisions. And git fetcher (and I suspect others
>>> too) starts fetching with deleting the target folder, so if destsuffix
>>> isn't unique, then only the last code prevails.
>> It was only a comment, that the solution inside your example doesn't
>> work in any case.
>>
>> In your example the Cargo.lock reference two crates inside the same
>> repository:
>>
>> [[package]]
>> name = "reqwest-middleware"
>> version = "0.4.2"
>> source =
>> "git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"
>>
>> [[package]]
>> name = "reqwest-retry"
>> version = "0.7.0"
>> source =
>> "git+https://github.com/astral-sh/reqwest-middleware?rev=7650ed76215a962a96d94a79be71c27bffde7ab2#7650ed76215a962a96d94a79be71c27bffde7ab2"
>>
>> (https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L3431)
>>
>> Luckily both use the same revision of the repository and you can use a
>> single SRC_URI:
>>
>> SRC_URI +=
>> "git://github.com/astral-sh/reqwest-middleware;protocol=https;name=reqwest-middleware;destsuffix=reqwest-middleware;branch=main"
>>
>> If they use different revisions you have to add two SRC_URIs with
>> different name and destsuffix. Furthermore you need to add the crates
>> from the correct destsuffix to the vendor folder because both checkouts
>> can contain the same crates.
>>
>> I follow the cargo vendor approach and use a part of the revision inside
>> the subdir (destsuffix).
>>
>> SRC_URI +=
>> "git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#7650ed7;destsuffix=cargo/git/checkouts/reqwest-middleware/7650ed7"
>> SRC_URI +=
>> "git://github.com/astral-sh/reqwest-middleware;protocol=https;nobranch=1;name=reqwest-middleware#591ab44;destsuffix=cargo/git/checkouts/reqwest-middleware/591ab44"
> Hmmm... ooops. You are definitely right - I do break crates if they have
> the same name, they are not differentiated and copied over each other.
> Thanks a lot - will take care of it in next version.
>
> Regarding the duplicated SRC_URI: yes, but that's on the user.
>
>>> If this should be
>>> enforced, I would rather put that logic into a new QA check, or maybe
>>> into the fetcher directly.
>>>
>>> (A bit more touched on your note below)
>>>
>>>>>>>>> Signed-off-by: Gyorgy Sarvari<skandigraun@gmail.com>
>>>>>>>>> Cc: Tom Geelen<t.f.g.geelen@gmail.com>
>>>>>>>>>
>>>>>>>>> ---
>>>>>>>>>     meta/classes-recipe/cargo_common.bbclass | 158 ++++++++++++++++-------
>>>>>>>>>     1 file changed, 108 insertions(+), 50 deletions(-)
>>>>>>>>>
>>>>>>>>> diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
>>>>>>>>> index c9eb2d09a5..79c1351298 100644
>>>>>>>>> --- a/meta/classes-recipe/cargo_common.bbclass
>>>>>>>>> +++ b/meta/classes-recipe/cargo_common.bbclass
>>>>>>>>> @@ -129,6 +129,44 @@ cargo_common_do_configure () {
>>>>>>>>>     python cargo_common_do_patch_paths() {
>>>>>>>>>         import shutil
>>>>>>>>>     
>>>>>>>>> +    def is_rust_crate_folder(path):
>>>>>>>>> +        cargo_toml_path = os.path.join(path, 'Cargo.toml')
>>>>>>>>> +        return os.path.exists(cargo_toml_path)
>>>>>>>>> +
>>>>>>>>> +    def load_toml_file(toml_path):
>>>>>>>>> +        import tomllib
>>>>>>>>> +        with open(toml_path, 'rb') as f:
>>>>>>>>> +            toml = tomllib.load(f)
>>>>>>>>> +        return toml
>>>>>>>>> +
>>>>>>>>> +    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
>>>>>>>>> +        for lf_repo in lockfile_repos.keys():
>>>>>>>>> +            if repo in lf_repo and lf_repo.endswith(revision):
>>>>>>>> Does this works if the URL contains a "rev" query parameter? This
>>>>>>>> happens if the same git repository is used with different revisions.
>>>>>>> I *think* yes, since I query the revision from the fetcher, instead of
>>>>>>> parsing it myself (and I use both the repo and revision for matching the
>>>>>>> cargo.lock repos). But will test it specifically, and make it work if it
>>>>>>> wouldn't work out of the box. Thanks for calling my attention on this.
>>>>>> The problem is that the source replacement key contains a query
>>>>>> parameter. The query isn't supported by the git fetcher. That means
>>>>>> you have to remove the query from the SRC_URI but add it back in the
>>>>>> source entry in the config.toml.
>>>>> You mean for dynamic fetching, from Cargo.lock? This patch still relies
>>>>> on the user adding these dependencies to the SRC_URI.
>>>>> Otherwise I might be misunderstanding your question...
>>>> Please check the source inside the Cargo.lock:
>>>> https://github.com/astral-sh/uv/blob/0.8.19/Cargo.lock#L302
>>>>
>>>> It contains a rev query parameter. This query parameter must be part
>>>> of the source key inside the config.toml:
>>>>
>>>> [source."git+https://github.com/astral-sh/rs-async-zip?rev=285e48742b74ab109887d62e1ae79e7c15fd4878"]
>>>>
>>>>
>>> Yes, it supposed to work like that already. The
>>> extract_git_repos_from_lockfile() collects the repos starting with
>>> "git+" from Cargo.lock - this value contains the rev parameter already.
>>> These values are only used as keys in the config.toml file, just like
>>> your example. Before adding it to config.toml, only the last optional
>>> part, the revision after the "#" is cut off. (E.g.
>>> "git+https://github.com/foo?rev=123#123" becomes only
>>> "git+https://github.com/foo?rev=123")
>> Okay, I use the SRC_URIs as source and therefore have the problem to
>> reconstruct the source key. The reason therefore where that I populate
>> the vendor folder during do_unpack. But the config.toml could be created
>> after do_patch and the Cargo.lock should be the better source.
> Personally my main goal would be only to add workspace support to the
> existing class without moving other pieces, but I think config.toml
> creation could be moved after do_patch without big issues, I don't see
> any obvious dependency problems.

The question is if we want to patch the git repository or the partial 
copy inside the vendor folder. In my case I populate the vendor folder 
during do_unpack to patch the vendor folder.

> I would still encourage you to submit your solution (if it is possible
> to open-source it) - it sounds it has been used in real world builds.
We still testing it but I have push the current version to github:

https://github.com/weidmueller/poky/tree/feature/implicit-urls-demo

>>> I do not use SRC_URI components in this file - currently there is some
>>> loose connection between SRC_URI and Cargo.lock repos: at the end I try
>>> to match each SRC_URI to a Cargo.lock repo, and see if the user has
>>> fetched every required repo (by trying to match the repo URL and
>>> revision), but in general the values are used differently.
>>>
>>>>>>>>> +                lockfile_repos[lf_repo] = True
>>>>>>>>> +                return lf_repo.split("#")[0]
>>>>>>>>> +        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
>>>>>>>>> +
>>>>>>>>> +    def create_cargo_checksum(folder_path):
>>>>>>>>> +        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
>>>>>>>>> +        if os.path.exists(checksum_path):
>>>>>>>>> +            return
>>>>>>>>> +
>>>>>>>>> +        import hashlib, json
>>>>>>>>> +
>>>>>>>>> +        checksum = {'files': {}}
>>>>>>>>> +        for root, _, files in os.walk(folder_path):
>>>>>>>>> +            for f in files:
>>>>>>>>> +                full_path = os.path.join(root, f)
>>>>>>>>> +                relative_path = os.path.relpath(full_path, folder_path)
>>>>>>>>> +                if relative_path.startswith(".git/"):
>>>>>>>>> +                    continue
>>>>>>>>> +                with open(full_path, 'rb') as f2:
>>>>>>>>> +                    file_sha = hashlib.sha256(f2.read()).hexdigest()
>>>>>>>>> +                checksum["files"][relative_path] = file_sha
>>>>>>>> Do we really need the calculation of the checksum?
>>>>>>> For source replacement AFAIK it is mandatory, otherwise cargo complains.
>>>>>>> (But I'd be happy to stand corrected)
>>>>>> Have you test an empty dictionary for "files" and NULL for "package"?
>>>>>>
>>>>> Are these valid states? Currently the checksum calculation happens for
>>>>> crate folders that have been actually copied to the vendor folder. And
>>>>> that happens only, in case there is at least a Cargo.toml manifest in
>>>>> that folder, so the files dict shouldn't be empty. Otherwise the
>>>>> checksum sub iterates through all the files it can find, it doesn't try
>>>>> to validate it against any manifests.
>>>> Do we need the validation by cargo? The crate fetcher skip the
>>>> validation with an empty dict and the same works for git sources.
>>>>
>>> Oh, you mean if there are no sources found that should be vendored? That
>>> was definitely a bug in this version, and v2 should have a check for
>>> that - if there is nothing to vendor than it stops and returns silently.
>> I mean that we already ensure the integrity of our sources. Why should
>> we invest time to populate the files checksum if we could simple skip
>> the check inside cargo by an empty files dictionary.
>>
>> metadata['files'] = {}
>> metadata['package'] = tarhash
>>
>> (https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L120)
>>
>> Maybe we should even remove the .cargo-checksum.json creation from the
>> fetcher and move it into your function. We can speed up the process if
>> we use the checksum from the Cargo.lock instead of the on-the-fly
>> calculation inside the fetcher:
>>
>> tarhash = hashlib.sha256(f.read()).hexdigest()
>>
>> (https://github.com/openembedded/bitbake/blob/master/lib/bb/fetch2/crate.py#L118)
> I haven't seen this part yet in the fetcher, but I can definitely look
> into it for simplification. Thanks - will try to add it to the next version.
>
>>>>>>>>> +
>>>>>>>>> +        with open(checksum_path, 'w') as f:
>>>>>>>>> +            json.dump(checksum, f)
>>>>>>>>> +
>>>>>>>>>         cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
>>>>>>>>>         if not os.path.exists(cargo_config):
>>>>>>>>>             return
>>>>>>>>> @@ -137,66 +175,86 @@ python cargo_common_do_patch_paths() {
>>>>>>>>>         if len(src_uri) == 0:
>>>>>>>>>             return
>>>>>>>>>     
>>>>>>>>> -    patches = dict()
>>>>>>>>> +    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>>>> +    if not os.path.exists(lockfile):
>>>>>>>>> +        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>>>> +
>>>>>>>>> +    lockfile = load_toml_file(lockfile)
>>>>>>>>> +
>>>>>>>>> +    # key is the repo url, value is a boolean, which is used later
>>>>>>>>> +    # to indicate if there is a matching repository in SRC_URI also
>>>>>>>>> +    lockfile_git_repos = {}
>>>>>>>>> +    for p in lockfile['package']:
>>>>>>>>> +        if 'source' in p and p['source'].startswith('git+'):
>>>>>>>>> +            lockfile_git_repos[p['source']] = False
>>>>>>>>> +
>>>>>>>>> +    sources = dict()
>>>>>>>>>         workdir = d.getVar('UNPACKDIR')
>>>>>>>>>         fetcher = bb.fetch2.Fetch(src_uri, d)
>>>>>>>>> +
>>>>>>>>> +    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
>>>>>>>>> +
>>>>>>>>> +    os.makedirs(vendor_folder)
>>>>>>>>> +
>>>>>>>>>         for url in fetcher.urls:
>>>>>>>>>             ud = fetcher.ud[url]
>>>>>>>>> -        if ud.type == 'git' or ud.type == 'gitsm':
>>>>>>>>> -            name = ud.parm.get('name')
>>>>>>>>> -            destsuffix = ud.parm.get('destsuffix')
>>>>>>>>> -            if name is not None and destsuffix is not None:
>>>>>>>>> -                if ud.user:
>>>>>>>>> -                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>>>> -                else:
>>>>>>>>> -                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>>>> -                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
>>>>>>>>> -                patches.setdefault(repo, []).append(path)
>>>>>>>>> +        if ud.type != 'git' and ud.type != 'gitsm':
>>>>>>>>> +            continue
>>>>>>>>>     
>>>>>>>>> -    with open(cargo_config, "a+") as config:
>>>>>>>>> -        for k, v in patches.items():
>>>>>>>>> -            print('\n[patch."%s"]' % k, file=config)
>>>>>>>>> -            for name in v:
>>>>>>>>> -                print(name, file=config)
>>>>>>>>> +        destsuffix = ud.parm.get('destsuffix')
>>>>>>>>> +        crate_folder = os.path.join(workdir, destsuffix)
>>>>>>>>>     
>>>>>>>>> -    if not patches:
>>>>>>>>> -        return
>>>>>>>>> +        if destsuffix is None or not is_rust_crate_folder(crate_folder):
>>>>>>>>> +            continue
>>>>>>>>>     
>>>>>>>>> -    # Cargo.lock file is needed for to be sure that artifacts
>>>>>>>>> -    # downloaded by the fetch steps are those expected by the
>>>>>>>>> -    # project and that the possible patches are correctly applied.
>>>>>>>>> -    # Moreover since we do not want any modification
>>>>>>>>> -    # of this file (for reproducibility purpose), we prevent it by
>>>>>>>>> -    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
>>>>>>>>> -    # here is better than letting cargo tell (in case the file is missing)
>>>>>>>>> -    # "Cargo.lock should be modified but --frozen was given"
>>>>>>>>> +        if ud.user:
>>>>>>>>> +            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
>>>>>>>>> +        else:
>>>>>>>>> +            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
>>>>>>>>>     
>>>>>>>>> -    lockfile = d.getVar("CARGO_LOCK_PATH")
>>>>>>>>> -    if not os.path.exists(lockfile):
>>>>>>>>> -        bb.fatal(f"{lockfile} file doesn't exist")
>>>>>>>>> +        sources[destsuffix] = (repo, ud.revision, crate_folder)
>>>>>>>>> +
>>>>>>>>> +        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
>>>>>>>>> +        cargo_toml = load_toml_file(cargo_toml_path)
>>>>>>>>> +
>>>>>>>>> +        if 'workspace' in cargo_toml:
>>>>>>>>> +            members = cargo_toml['workspace']['members']
>>>>>>>>> +            for member in members:
>>>>>>>>> +                member_crate_folder = os.path.join(workdir, destsuffix, member)
>>>>>>>>> +                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
>>>>>>>>> +                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
>>>>>>>>> +                member_crate_name = member_cargo_toml['package']['name']
>>>>>>>>> +                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
>>>>>>>>> +
>>>>>>>>> +        if 'package' in cargo_toml:
>>>>>>>>> +            crate_folder = os.path.join(workdir, destsuffix)
>>>>>>>>> +            crate_name = cargo_toml['package']['name']
>>>>>>>>> +            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
>>>>>>>>> +
>>>>>>>>> +    for d in os.scandir(vendor_folder):
>>>>>>>>> +        if d.is_dir():
>>>>>>>>> +            create_cargo_checksum(d.path)
>>>>>>>>> +
>>>>>>>>> +
>>>>>>>>> +    with open(cargo_config, "a+") as config:
>>>>>>>>> +        print('\n[source."yocto-vendored-sources"]', file=config)
>>>>>>>>> +        print('directory = "%s"' % vendor_folder, file=config)
>>>>>>>>> +
>>>>>>>>> +        for destsuffix, (repo, revision, repo_path) in sources.items():
>>>>>>>>> +            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
>>>>>>>>> +            print('\n[source."%s"]' % lockfile_repo, file=config)
>>>>>>>>> +            print('git = "%s"' % repo, file=config)
>>>>>>>>> +            print('rev = "%s"' % revision, file=config)
>>>>>>>>> +            print('replace-with = "yocto-vendored-sources"', file=config)
>>>>>>>>> +
>>>>>>>>> +    # check if there are any git repos in the lock file that were not visited
>>>>>>>>> +    # in the previous loop, when the source replacement was created, and warn about it
>>>>>>>>> +    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
>>>>>>>>> +        if not found_in_src_uri:
>>>>>>>>> +            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
>>>>>>>>>     
>>>>>>>>> -    # There are patched files and so Cargo.lock should be modified but we use
>>>>>>>>> -    # --frozen so let's handle that modifications here.
>>>>>>>>> -    #
>>>>>>>>> -    # Note that a "better" (more elegant ?) would have been to use cargo update for
>>>>>>>>> -    # patched packages:
>>>>>>>>> -    #  cargo update --offline -p package_1 -p package_2
>>>>>>>>> -    # But this is not possible since it requires that cargo local git db
>>>>>>>>> -    # to be populated and this is not the case as we fetch git repo ourself.
>>>>>>>>> -
>>>>>>>>> -    lockfile_orig = lockfile + ".orig"
>>>>>>>>> -    if not os.path.exists(lockfile_orig):
>>>>>>>>> -        shutil.copy(lockfile, lockfile_orig)
>>>>>>>>> -
>>>>>>>>> -    newlines = []
>>>>>>>>> -    with open(lockfile_orig, "r") as f:
>>>>>>>>> -        for line in f.readlines():
>>>>>>>>> -            if not line.startswith("source = \"git"):
>>>>>>>>> -                newlines.append(line)
>>>>>>>>> -
>>>>>>>>> -    with open(lockfile, "w") as f:
>>>>>>>>> -        f.writelines(newlines)
>>>>>>>>>     }
>>>>>>>>> +
>>>>>>>>>     do_configure[postfuncs] += "cargo_common_do_patch_paths"
>>>>>>>>>     
>>>>>>>>>     do_compile:prepend () {
>>>>>>>>>
>>>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>>>> Links: You receive all messages sent to this group.
>>>>>>>>> View/Reply Online (#224426):https://lists.openembedded.org/g/openembedded-core/message/224426
>>>>>>>>> Mute This Topic:https://lists.openembedded.org/mt/115578466/6374899
>>>>>>>>> Group Owner:openembedded-core+owner@lists.openembedded.org
>>>>>>>>> Unsubscribe:https://lists.openembedded.org/g/openembedded-core/unsub [stefan.herbrechtsmeier-oss@weidmueller.com]
>>>>>>>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>>>>>>>
diff mbox series

Patch

diff --git a/meta/classes-recipe/cargo_common.bbclass b/meta/classes-recipe/cargo_common.bbclass
index c9eb2d09a5..79c1351298 100644
--- a/meta/classes-recipe/cargo_common.bbclass
+++ b/meta/classes-recipe/cargo_common.bbclass
@@ -129,6 +129,44 @@  cargo_common_do_configure () {
 python cargo_common_do_patch_paths() {
     import shutil
 
+    def is_rust_crate_folder(path):
+        cargo_toml_path = os.path.join(path, 'Cargo.toml')
+        return os.path.exists(cargo_toml_path)
+
+    def load_toml_file(toml_path):
+        import tomllib
+        with open(toml_path, 'rb') as f:
+            toml = tomllib.load(f)
+        return toml
+
+    def get_matching_repo_from_lockfile(lockfile_repos, repo, revision):
+        for lf_repo in lockfile_repos.keys():
+            if repo in lf_repo and lf_repo.endswith(revision):
+                lockfile_repos[lf_repo] = True
+                return lf_repo.split("#")[0]
+        bb.fatal('Cannot find %s (%s) repository from SRC_URI in Cargo.lock file' % (repo, revision))
+
+    def create_cargo_checksum(folder_path):
+        checksum_path = os.path.join(folder_path, '.cargo-checksum.json')
+        if os.path.exists(checksum_path):
+            return
+
+        import hashlib, json
+
+        checksum = {'files': {}}
+        for root, _, files in os.walk(folder_path):
+            for f in files:
+                full_path = os.path.join(root, f)
+                relative_path = os.path.relpath(full_path, folder_path)
+                if relative_path.startswith(".git/"):
+                    continue
+                with open(full_path, 'rb') as f2:
+                    file_sha = hashlib.sha256(f2.read()).hexdigest()
+                checksum["files"][relative_path] = file_sha
+
+        with open(checksum_path, 'w') as f:
+            json.dump(checksum, f)
+
     cargo_config = os.path.join(d.getVar("CARGO_HOME"), "config.toml")
     if not os.path.exists(cargo_config):
         return
@@ -137,66 +175,86 @@  python cargo_common_do_patch_paths() {
     if len(src_uri) == 0:
         return
 
-    patches = dict()
+    lockfile = d.getVar("CARGO_LOCK_PATH")
+    if not os.path.exists(lockfile):
+        bb.fatal(f"{lockfile} file doesn't exist")
+
+    lockfile = load_toml_file(lockfile)
+
+    # key is the repo url, value is a boolean, which is used later
+    # to indicate if there is a matching repository in SRC_URI also
+    lockfile_git_repos = {}
+    for p in lockfile['package']:
+        if 'source' in p and p['source'].startswith('git+'):
+            lockfile_git_repos[p['source']] = False
+
+    sources = dict()
     workdir = d.getVar('UNPACKDIR')
     fetcher = bb.fetch2.Fetch(src_uri, d)
+
+    vendor_folder = os.path.join(workdir, 'yocto-vendored-source-crates')
+
+    os.makedirs(vendor_folder)
+
     for url in fetcher.urls:
         ud = fetcher.ud[url]
-        if ud.type == 'git' or ud.type == 'gitsm':
-            name = ud.parm.get('name')
-            destsuffix = ud.parm.get('destsuffix')
-            if name is not None and destsuffix is not None:
-                if ud.user:
-                    repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
-                else:
-                    repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
-                path = '%s = { path = "%s" }' % (name, os.path.join(workdir, destsuffix))
-                patches.setdefault(repo, []).append(path)
+        if ud.type != 'git' and ud.type != 'gitsm':
+            continue
 
-    with open(cargo_config, "a+") as config:
-        for k, v in patches.items():
-            print('\n[patch."%s"]' % k, file=config)
-            for name in v:
-                print(name, file=config)
+        destsuffix = ud.parm.get('destsuffix')
+        crate_folder = os.path.join(workdir, destsuffix)
 
-    if not patches:
-        return
+        if destsuffix is None or not is_rust_crate_folder(crate_folder):
+            continue
 
-    # Cargo.lock file is needed for to be sure that artifacts
-    # downloaded by the fetch steps are those expected by the
-    # project and that the possible patches are correctly applied.
-    # Moreover since we do not want any modification
-    # of this file (for reproducibility purpose), we prevent it by
-    # using --frozen flag (in CARGO_BUILD_FLAGS) and raise a clear error
-    # here is better than letting cargo tell (in case the file is missing)
-    # "Cargo.lock should be modified but --frozen was given"
+        if ud.user:
+            repo = '%s://%s@%s%s' % (ud.proto, ud.user, ud.host, ud.path)
+        else:
+            repo = '%s://%s%s' % (ud.proto, ud.host, ud.path)
 
-    lockfile = d.getVar("CARGO_LOCK_PATH")
-    if not os.path.exists(lockfile):
-        bb.fatal(f"{lockfile} file doesn't exist")
+        sources[destsuffix] = (repo, ud.revision, crate_folder)
+
+        cargo_toml_path = os.path.join(workdir, destsuffix, 'Cargo.toml')
+        cargo_toml = load_toml_file(cargo_toml_path)
+
+        if 'workspace' in cargo_toml:
+            members = cargo_toml['workspace']['members']
+            for member in members:
+                member_crate_folder = os.path.join(workdir, destsuffix, member)
+                member_crate_cargo_toml = os.path.join(member_crate_folder, 'Cargo.toml')
+                member_cargo_toml = load_toml_file(member_crate_cargo_toml)
+                member_crate_name = member_cargo_toml['package']['name']
+                shutil.copytree(member_crate_folder, os.path.join(vendor_folder, member_crate_name))
+
+        if 'package' in cargo_toml:
+            crate_folder = os.path.join(workdir, destsuffix)
+            crate_name = cargo_toml['package']['name']
+            shutil.copytree(crate_folder, os.path.join(vendor_folder, crate_name))
+
+    for d in os.scandir(vendor_folder):
+        if d.is_dir():
+            create_cargo_checksum(d.path)
+
+
+    with open(cargo_config, "a+") as config:
+        print('\n[source."yocto-vendored-sources"]', file=config)
+        print('directory = "%s"' % vendor_folder, file=config)
+
+        for destsuffix, (repo, revision, repo_path) in sources.items():
+            lockfile_repo = get_matching_repo_from_lockfile(lockfile_git_repos, repo, revision)
+            print('\n[source."%s"]' % lockfile_repo, file=config)
+            print('git = "%s"' % repo, file=config)
+            print('rev = "%s"' % revision, file=config)
+            print('replace-with = "yocto-vendored-sources"', file=config)
+
+    # check if there are any git repos in the lock file that were not visited
+    # in the previous loop, when the source replacement was created, and warn about it
+    for lf_repo, found_in_src_uri in lockfile_git_repos.items():
+        if not found_in_src_uri:
+            bb.warn(f"{lf_repo} is present in lockfile, but not found in SRC_URI")
 
-    # There are patched files and so Cargo.lock should be modified but we use
-    # --frozen so let's handle that modifications here.
-    #
-    # Note that a "better" (more elegant ?) would have been to use cargo update for
-    # patched packages:
-    #  cargo update --offline -p package_1 -p package_2
-    # But this is not possible since it requires that cargo local git db
-    # to be populated and this is not the case as we fetch git repo ourself.
-
-    lockfile_orig = lockfile + ".orig"
-    if not os.path.exists(lockfile_orig):
-        shutil.copy(lockfile, lockfile_orig)
-
-    newlines = []
-    with open(lockfile_orig, "r") as f:
-        for line in f.readlines():
-            if not line.startswith("source = \"git"):
-                newlines.append(line)
-
-    with open(lockfile, "w") as f:
-        f.writelines(newlines)
 }
+
 do_configure[postfuncs] += "cargo_common_do_patch_paths"
 
 do_compile:prepend () {