All of lore.kernel.org
 help / color / mirror / Atom feed
* Dunfell, nodejs and typescript - short experience report
@ 2021-02-12 10:02 Simon Vogl
  2021-02-12 10:12 ` [yocto] " Josef Holzmayr
  0 siblings, 1 reply; 6+ messages in thread
From: Simon Vogl @ 2021-02-12 10:02 UTC (permalink / raw)
  To: yocto

[-- Attachment #1: Type: text/plain, Size: 3507 bytes --]

I have some remarks and questions about the npm/nodejs support in 
dunfell that I wanted to share. We are creating nodejs-based IoT edge 
solutions and upgrading our build environments to Dunfell one by one. In 
the course of this, we are switching to the new npm-implementation and 
found a few small issues.

Firstly, the do_configure() task takes quite some time to complete. 
After a quick analysis, I saw that most of the time is being spent in 
creating the npmrc files while packing the dependent packages. I wrote a 
small workaround to directly create the file instead of calling 'npm 
config', which results in a 3x-4x speedup:

Signed-off-by: Simon Vogl<simon@voxel.at>
---
  lib/bb/fetch2/npm.py | 9 +++++++--
  1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/lib/bb/fetch2/npm.py b/lib/bb/fetch2/npm.py
index 4789850..2720d87 100644
--- a/lib/bb/fetch2/npm.py
+++ b/lib/bb/fetch2/npm.py
@@ -97,13 +97,18 @@ class NpmEnvironment(object):
                  cmd = "NPM_CONFIG_GLOBALCONFIG=%s " % cfgfile + cmd
                  return runfetchcmd(cmd, d, workdir=workdir)
  
+            cfg = open(cfgfile, "a")
              if self.configs:
                  for key, value in self.configs:
-                    _run("npm config set %s %s" % (key, shlex.quote(value)))
+                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
+                    #_run("npm config set %s %s" % (key, shlex.quote(value)))
  
              if configs:
                  for key, value in configs:
-                    _run("npm config set %s %s" % (key, shlex.quote(value)))
+                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
+                    # _run("npm config set %s %s" % (key, shlex.quote(value)))
+
+            cfg.close()
  
              if args:
                  for key, value in args:
-- 2.7.4

Are there any side effects that I did not stumble over yet? And I'd LOVE 
to have these calls running in a thread-pool for better performance...


Secondly, our projects are based on typescript, so a native compile step 
is necessary to create a compiled version for packing. We experimented 
with a separate release branch to check in compiled versions, but this 
is not easy to handle. I played around with npm.bbclass and found a way 
to extend configure (!) with a call to our build script before packaging:

diff --git a/meta/classes/npm.bbclass b/meta/classes/npm.bbclass
index 068032a1e5..31535098cf 100644
--- a/meta/classes/npm.bbclass
+++ b/meta/classes/npm.bbclass
@@ -170,6 +170,11 @@ python npm_do_configure() {
  
      # Configure the main package
      with tempfile.TemporaryDirectory() as tmpdir:
+        # install all (native) build dependencies, overrides npm cache:
+        ret = os.system("npm i")
+        # run build step:
+        env.run("npm run build", args=[], workdir=d.getVar("S"))
+
          tarball = npm_pack(env, d.getVar("S"), tmpdir)
          npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)

As we have plain JS packages as well, I put the modified configure() in 
a subclass and this works for us, but it does not look like a clean 
solution to me. How do you other IoT'ers address this situation?

Simon


-- 
VoXel Interaction Design  |  www.voxel.at
DI Dr.techn. Simon Vogl   |  simon@voxel.at
Tomaschekweg 46           |  +43 650 2323 555
A-4040 Linz - Austria     |
Office address: Industriezeile 35, 4020 Linz (2nd floor)


[-- Attachment #2: Type: text/html, Size: 4353 bytes --]

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [yocto] Dunfell, nodejs and typescript - short experience report
  2021-02-12 10:02 Dunfell, nodejs and typescript - short experience report Simon Vogl
@ 2021-02-12 10:12 ` Josef Holzmayr
  2021-02-12 10:25   ` Simon Vogl
  0 siblings, 1 reply; 6+ messages in thread
From: Josef Holzmayr @ 2021-02-12 10:12 UTC (permalink / raw)
  To: Simon Vogl; +Cc: yocto

Howdy!

Thanks for sharing, a few comments inline.

Am Fr., 12. Feb. 2021 um 11:03 Uhr schrieb Simon Vogl <simon@voxel.at>:
>
> I have some remarks and questions about the npm/nodejs support in dunfell that I wanted to share. We are creating nodejs-based IoT edge solutions and upgrading our build environments to Dunfell one by one. In the course of this, we are switching to the new npm-implementation and found a few small issues.
>
> Firstly, the do_configure() task takes quite some time to complete. After a quick analysis, I saw that most of the time is being spent in creating the npmrc files while packing the dependent packages. I wrote a small workaround to directly create the file instead of calling 'npm config', which results in a 3x-4x speedup:
>
> Signed-off-by: Simon Vogl <simon@voxel.at>
> ---
>  lib/bb/fetch2/npm.py | 9 +++++++--
>  1 file changed, 7 insertions(+), 2 deletions(-)
>
> diff --git a/lib/bb/fetch2/npm.py b/lib/bb/fetch2/npm.py
> index 4789850..2720d87 100644
> --- a/lib/bb/fetch2/npm.py
> +++ b/lib/bb/fetch2/npm.py
> @@ -97,13 +97,18 @@ class NpmEnvironment(object):
>                  cmd = "NPM_CONFIG_GLOBALCONFIG=%s " % cfgfile + cmd
>                  return runfetchcmd(cmd, d, workdir=workdir)
>
> +            cfg = open(cfgfile, "a")
>              if self.configs:
>                  for key, value in self.configs:
> -                    _run("npm config set %s %s" % (key, shlex.quote(value)))
> +                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
> +                    #_run("npm config set %s %s" % (key, shlex.quote(value)))
>
>              if configs:
>                  for key, value in configs:
> -                    _run("npm config set %s %s" % (key, shlex.quote(value)))
> +                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
> +                    # _run("npm config set %s %s" % (key, shlex.quote(value)))
> +
> +            cfg.close()
>
>              if args:
>                  for key, value in args:
> --
> 2.7.4
>
> Are there any side effects that I did not stumble over yet? And I'd LOVE to have these calls running in a thread-pool for better performance...

The main side effect is that you're effectively patching poky, which
is bad for maintenance.

> Secondly, our projects are based on typescript, so a native compile step is necessary to create a compiled version for packing. We experimented with a separate release branch to check in compiled versions, but this is not easy to handle. I played around with npm.bbclass and found a way to extend configure (!) with a call to our build script before packaging:
>
> diff --git a/meta/classes/npm.bbclass b/meta/classes/npm.bbclass
> index 068032a1e5..31535098cf 100644
> --- a/meta/classes/npm.bbclass
> +++ b/meta/classes/npm.bbclass
> @@ -170,6 +170,11 @@ python npm_do_configure() {
>
>      # Configure the main package
>      with tempfile.TemporaryDirectory() as tmpdir:
> +        # install all (native) build dependencies, overrides npm cache:
> +        ret = os.system("npm i")
> +        # run build step:
> +        env.run("npm run build", args=[], workdir=d.getVar("S"))
> +
>          tarball = npm_pack(env, d.getVar("S"), tmpdir)
>          npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
>
> As we have plain JS packages as well, I put the modified configure() in a subclass and this works for us, but it does not look like a clean solution to me. How do you other IoT'ers address this situation?

Again, patching poky is a bad idea. Creating custom bbclasses is much
neater: you could create a base include, and pull that together with
npm.bbclass into two final bbclasses of yours, like npm-js-voxel and
npm-ts-voxel. The former would not have the compilation step. And,
putting the typescript/webpack invocation into configure is also not
exactly how things are meant to work. I know that the dependency
tracking of npm is not easily compatible with bitbake, but the aim
should be to
1) have a recipe that provides typescript-native
2) DEPEND on typescript-native in the recipe which you need to compile
3) add a do_compile stage that does the work.

Greetz

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [yocto] Dunfell, nodejs and typescript - short experience report
  2021-02-12 10:12 ` [yocto] " Josef Holzmayr
@ 2021-02-12 10:25   ` Simon Vogl
  2021-02-24 11:02     ` TRO
  0 siblings, 1 reply; 6+ messages in thread
From: Simon Vogl @ 2021-02-12 10:25 UTC (permalink / raw)
  To: Josef Holzmayr; +Cc: yocto

[-- Attachment #1: Type: text/plain, Size: 5349 bytes --]

Hi,


Am 12.02.21 um 11:12 schrieb Josef Holzmayr:
> Howdy!
>
> Thanks for sharing, a few comments inline.
>
> Am Fr., 12. Feb. 2021 um 11:03 Uhr schrieb Simon Vogl <simon@voxel.at>:
>> I have some remarks and questions about the npm/nodejs support in dunfell that I wanted to share. We are creating nodejs-based IoT edge solutions and upgrading our build environments to Dunfell one by one. In the course of this, we are switching to the new npm-implementation and found a few small issues.
>>
>> Firstly, the do_configure() task takes quite some time to complete. After a quick analysis, I saw that most of the time is being spent in creating the npmrc files while packing the dependent packages. I wrote a small workaround to directly create the file instead of calling 'npm config', which results in a 3x-4x speedup:
>>
>> Signed-off-by: Simon Vogl <simon@voxel.at>
>> ---
>>   lib/bb/fetch2/npm.py | 9 +++++++--
>>   1 file changed, 7 insertions(+), 2 deletions(-)
>>
>> diff --git a/lib/bb/fetch2/npm.py b/lib/bb/fetch2/npm.py
>> index 4789850..2720d87 100644
>> --- a/lib/bb/fetch2/npm.py
>> +++ b/lib/bb/fetch2/npm.py
>> @@ -97,13 +97,18 @@ class NpmEnvironment(object):
>>                   cmd = "NPM_CONFIG_GLOBALCONFIG=%s " % cfgfile + cmd
>>                   return runfetchcmd(cmd, d, workdir=workdir)
>>
>> +            cfg = open(cfgfile, "a")
>>               if self.configs:
>>                   for key, value in self.configs:
>> -                    _run("npm config set %s %s" % (key, shlex.quote(value)))
>> +                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
>> +                    #_run("npm config set %s %s" % (key, shlex.quote(value)))
>>
>>               if configs:
>>                   for key, value in configs:
>> -                    _run("npm config set %s %s" % (key, shlex.quote(value)))
>> +                    cfg.write("%s=%s\n" % (key, shlex.quote(value)))
>> +                    # _run("npm config set %s %s" % (key, shlex.quote(value)))
>> +
>> +            cfg.close()
>>
>>               if args:
>>                   for key, value in args:
>> --
>> 2.7.4
>>
>> Are there any side effects that I did not stumble over yet? And I'd LOVE to have these calls running in a thread-pool for better performance...
> The main side effect is that you're effectively patching poky, which
> is bad for maintenance.
I know, that's why I'm asking in the first place. But performance here 
is really really improvable.
>> Secondly, our projects are based on typescript, so a native compile step is necessary to create a compiled version for packing. We experimented with a separate release branch to check in compiled versions, but this is not easy to handle. I played around with npm.bbclass and found a way to extend configure (!) with a call to our build script before packaging:
>>
>> diff --git a/meta/classes/npm.bbclass b/meta/classes/npm.bbclass
>> index 068032a1e5..31535098cf 100644
>> --- a/meta/classes/npm.bbclass
>> +++ b/meta/classes/npm.bbclass
>> @@ -170,6 +170,11 @@ python npm_do_configure() {
>>
>>       # Configure the main package
>>       with tempfile.TemporaryDirectory() as tmpdir:
>> +        # install all (native) build dependencies, overrides npm cache:
>> +        ret = os.system("npm i")
>> +        # run build step:
>> +        env.run("npm run build", args=[], workdir=d.getVar("S"))
>> +
>>           tarball = npm_pack(env, d.getVar("S"), tmpdir)
>>           npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
>>
>> As we have plain JS packages as well, I put the modified configure() in a subclass and this works for us, but it does not look like a clean solution to me. How do you other IoT'ers address this situation?
> Again, patching poky is a bad idea. Creating custom bbclasses is much
> neater: you could create a base include, and pull that together with
> npm.bbclass into two final bbclasses of yours, like npm-js-voxel and
> npm-ts-voxel. The former would not have the compilation step. And,
> putting the typescript/webpack invocation into configure is also not
> exactly how things are meant to work. I know that the dependency
> tracking of npm is not easily compatible with bitbake, but the aim
> should be to
> 1) have a recipe that provides typescript-native
> 2) DEPEND on typescript-native in the recipe which you need to compile
> 3) add a do_compile stage that does the work.

Agreed, and I have a patched configure in my own subclass without 
changing the official codebase -- I just wanted to point where the 
modification needs to take place.

I actually tried the approach that you propose by playing around with 
configure_append / compile_prepend tasks, but these build steps are 
called after the package has already been packed --> the compiled data 
is not being  installed, I'd have to re-pack things.

Agreed, a typescript-native package would be nice, on the other hand 
this is where the npm-version-chaos comes in again: Many packages use 
different tsc versions,...

Simon


> Greetz
>
> 
>
-- 
VoXel Interaction Design  |  www.voxel.at
DI Dr.techn. Simon Vogl   |  simon@voxel.at
Tomaschekweg 46           |  +43 650 2323 555
A-4040 Linz - Austria     |
Office address: Industriezeile 35, 4020 Linz (2nd floor)


[-- Attachment #2: Type: text/html, Size: 6774 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Dunfell, nodejs and typescript - short experience report
  2021-02-12 10:25   ` Simon Vogl
@ 2021-02-24 11:02     ` TRO
  2021-02-27 14:34       ` [yocto] " Simon Vogl
  0 siblings, 1 reply; 6+ messages in thread
From: TRO @ 2021-02-24 11:02 UTC (permalink / raw)
  To: yocto

[-- Attachment #1: Type: text/plain, Size: 408 bytes --]

Hi Simon,
I'm dealing actually with the same problem. Would you like to share your  "configure in my own subclass"?

I'm also thinking there is a need for a bbclass which actually is not using gyp, instead it should be able to "npm run build".

There is alsa a patch for speeding up npm npmsw fetcher https://www.mail-archive.com/openembedded-core@lists.openembedded.org/msg142406.html
cheers Thomas

[-- Attachment #2: Type: text/html, Size: 440 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [yocto] Dunfell, nodejs and typescript - short experience report
  2021-02-24 11:02     ` TRO
@ 2021-02-27 14:34       ` Simon Vogl
  2021-03-01  7:51         ` TRO
  0 siblings, 1 reply; 6+ messages in thread
From: Simon Vogl @ 2021-02-27 14:34 UTC (permalink / raw)
  To: TRO, yocto


[-- Attachment #1.1: Type: text/plain, Size: 1232 bytes --]

Sure, please find the bbclass file attached to this mail. All it does is 
to inherit npm and overwrite configure with an extended copy .

Meanwhile, I saw another issue popping up: It seems that multiple 
indirect dependencies to a package with different versions create 
inconsitent license checksum entries. In my case, several packages 
depend on 'xtend' in versions 4.0.0 to 4.0.2, the license file picked is 
package.json which contains the version, of course, and causes 
conflicts. Oh my.

Simon



Am 24.02.21 um 12:02 schrieb TRO:
>
> Hi Simon,
> I'm dealing actually with the same problem. Would you like to share 
> your  "configure in my own subclass"?
>
> I'm also thinking there is a need for a bbclass which actually is not 
> using gyp, instead it should be able to "npm run build".
>
> There is alsa a patch for speeding up npm npmsw fetcher 
> https://www.mail-archive.com/openembedded-core@lists.openembedded.org/msg142406.html
> cheers Thomas
>
>
> 
>
-- 
VoXel Interaction Design  |  www.voxel.at
DI Dr.techn. Simon Vogl   |  simon@voxel.at
Tomaschekweg 46           |  +43 650 2323 555
A-4040 Linz - Austria     |
Office address: Industriezeile 35, 4020 Linz (2nd floor)


[-- Attachment #1.2: Type: text/html, Size: 2206 bytes --]

[-- Attachment #2: npm_run_build.bbclass --]
[-- Type: text/plain, Size: 5857 bytes --]

# Copyright (C) 2020 Savoir-Faire Linux
#
# SPDX-License-Identifier: GPL-2.0-only
#
# This bbclass builds and installs an npm package to the target. The package
# sources files should be fetched in the calling recipe by using the SRC_URI
# variable. The ${S} variable should be updated depending of your fetcher.
#
# Usage:
#  SRC_URI = "..."
#  inherit npm_run_build
#
# ... for the rest see npm.bbclass


inherit npm

python npm_run_build_do_configure() {
    """
    Step one: configure the npm cache and the main npm package

    Every dependencies have been fetched and patched in the source directory.
    They have to be packed (this remove unneeded files) and added to the npm
    cache to be available for the next step.

    The main package and its associated manifest file and shrinkwrap file have
    to be configured to take into account these cached dependencies.

    [SV] copied from npm.bbclass and extended to run npm i, npm run build before 
    packing the main package.
    """
    import base64
    import copy
    import json
    import re
    import shlex
    import tempfile
    from bb.fetch2.npm import NpmEnvironment
    from bb.fetch2.npm import npm_unpack
    from bb.fetch2.npmsw import foreach_dependencies
    from bb.progress import OutOfProgressHandler

    bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
    bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)

    env = NpmEnvironment(d, configs=npm_global_configs(d))

    def _npm_cache_add(tarball):
        """Run 'npm cache add' for a specified tarball"""
        cmd = "npm cache add %s" % shlex.quote(tarball)
        env.run(cmd)

    def _npm_integrity(tarball):
        """Return the npm integrity of a specified tarball"""
        sha512 = bb.utils.sha512_file(tarball)
        return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()

    def _npm_version(tarball):
        """Return the version of a specified tarball"""
        regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
        return re.search(regex, tarball).group(1)

    def _npmsw_dependency_dict(orig, deptree):
        """
        Return the sub dictionary in the 'orig' dictionary corresponding to the
        'deptree' dependency tree. This function follows the shrinkwrap file
        format.
        """
        ptr = orig
        for dep in deptree:
            if "dependencies" not in ptr:
                ptr["dependencies"] = {}
            ptr = ptr["dependencies"]
            if dep not in ptr:
                ptr[dep] = {}
            ptr = ptr[dep]
        return ptr

    # Manage the manifest file and shrinkwrap files
    orig_manifest_file = d.expand("${S}/package.json")
    orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
    cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
    cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")

    with open(orig_manifest_file, "r") as f:
        orig_manifest = json.load(f)

    cached_manifest = copy.deepcopy(orig_manifest)
    cached_manifest.pop("dependencies", None)
    cached_manifest.pop("devDependencies", None)

    with open(orig_shrinkwrap_file, "r") as f:
        orig_shrinkwrap = json.load(f)

    cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
    cached_shrinkwrap.pop("dependencies", None)

    # Manage the dependencies
    progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
    progress_total = 1 # also count the main package
    progress_done = 0

    def _count_dependency(name, params, deptree):
        nonlocal progress_total
        progress_total += 1

    def _cache_dependency(name, params, deptree):
        destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
        destsuffix = os.path.join(*destsubdirs)
        with tempfile.TemporaryDirectory() as tmpdir:
            # Add the dependency to the npm cache
            destdir = os.path.join(d.getVar("S"), destsuffix)
            tarball = npm_pack(env, destdir, tmpdir)
            _npm_cache_add(tarball)
            # Add its signature to the cached shrinkwrap
            dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
            dep["version"] = _npm_version(tarball)
            dep["integrity"] = _npm_integrity(tarball)
            if params.get("dev", False):
                dep["dev"] = True
            # Display progress
            nonlocal progress_done
            progress_done += 1
            progress.write("%d/%d" % (progress_done, progress_total))

    dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
    foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
    foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)

    # Configure the main package
    with tempfile.TemporaryDirectory() as tmpdir:
        # install all (native) build dependencies:
        print("RUNNING NPM BUILD STEP %s" % os.getcwd() )
        ret = os.system("npm i")
        print("ret %d" % ret)
        # run build step:
        out = env.run("npm run build", args=[], workdir=d.getVar("S"))
        print(out)
        tarball = npm_pack(env, d.getVar("S"), tmpdir)
        npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)

    # Configure the cached manifest file and cached shrinkwrap file
    def _update_manifest(depkey):
        for name in orig_manifest.get(depkey, {}):
            version = cached_shrinkwrap["dependencies"][name]["version"]
            if depkey not in cached_manifest:
                cached_manifest[depkey] = {}
            cached_manifest[depkey][name] = version

    _update_manifest("dependencies")

    if dev:
        _update_manifest("devDependencies")

    with open(cached_manifest_file, "w") as f:
        json.dump(cached_manifest, f, indent=2)

    with open(cached_shrinkwrap_file, "w") as f:
        json.dump(cached_shrinkwrap, f, indent=2)
}

EXPORT_FUNCTIONS do_configure

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Dunfell, nodejs and typescript - short experience report
  2021-02-27 14:34       ` [yocto] " Simon Vogl
@ 2021-03-01  7:51         ` TRO
  0 siblings, 0 replies; 6+ messages in thread
From: TRO @ 2021-03-01  7:51 UTC (permalink / raw)
  To: yocto

[-- Attachment #1: Type: text/plain, Size: 526 bytes --]

Hi Simon,
thank you - in my current solution I don't use npm bbclass at all.
I basically use npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json;dev=True
this will downloadall npm stuff including angular because of dev=True to $S/node_modules

do_compile () {
#    build frontend
chmod -R a+w ${S}/node_modules/@angular
chmod 755 ${S}/node_modules/@angular/cli/bin/ng
cd ${S}/ && ./node_modules/@angular/cli/bin/ng build --prod
}

I'm dealing with that problem:
https://lists.yoctoproject.org/g/yocto/message/52515

[-- Attachment #2: Type: text/html, Size: 675 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2021-03-01  7:51 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-02-12 10:02 Dunfell, nodejs and typescript - short experience report Simon Vogl
2021-02-12 10:12 ` [yocto] " Josef Holzmayr
2021-02-12 10:25   ` Simon Vogl
2021-02-24 11:02     ` TRO
2021-02-27 14:34       ` [yocto] " Simon Vogl
2021-03-01  7:51         ` TRO

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.